From cdcc860967f89fe6abf3a3877a59cb6c23caba28 Mon Sep 17 00:00:00 2001
From: Oscar Gustafsson <oscar.gustafsson@gmail.com>
Date: Tue, 16 May 2023 10:10:24 +0200
Subject: [PATCH] Add branch node in architecture and fix issue with multiple
 destinations

---
 b_asic/architecture.py                        | 75 +++++++++++++------
 examples/folding_example_with_architecture.py | 23 +-----
 .../secondorderdirectformiir_architecture.py  | 57 +++++++-------
 3 files changed, 84 insertions(+), 71 deletions(-)

diff --git a/b_asic/architecture.py b/b_asic/architecture.py
index 40ff4cb2..8475b060 100644
--- a/b_asic/architecture.py
+++ b/b_asic/architecture.py
@@ -83,6 +83,13 @@ class HardwareBlock:
     def _repr_png_(self):
         return self._digraph()._repr_mimebundle_(include=["image/png"])["image/png"]
 
+    def _repr_svg_(self):
+        return self._digraph()._repr_mimebundle_(include=["image/svg+xml"])[
+            "image/svg+xml"
+        ]
+
+    _repr_html_ = _repr_svg_
+
     @property
     def entity_name(self) -> str:
         if self._entity_name is None:
@@ -488,8 +495,12 @@ of :class:`~b_asic.architecture.ProcessingElement`
         )
         self._memories = [memories] if isinstance(memories, Memory) else list(memories)
         self._direct_interconnects = direct_interconnects
-        self._variable_inport_to_resource: Dict[InputPort, Tuple[Resource, int]] = {}
-        self._variable_outport_to_resource: Dict[OutputPort, Tuple[Resource, int]] = {}
+        self._variable_inport_to_resource: DefaultDict[
+            InputPort, Set[Tuple[Resource, int]]
+        ] = defaultdict(set)
+        self._variable_outport_to_resource: DefaultDict[
+            OutputPort, Set[Tuple[Resource, int]]
+        ] = defaultdict(set)
         self._operation_inport_to_resource: Dict[InputPort, Resource] = {}
         self._operation_outport_to_resource: Dict[OutputPort, Resource] = {}
 
@@ -513,10 +524,14 @@ of :class:`~b_asic.architecture.ProcessingElement`
         return schedule_times.pop()
 
     def _build_dicts(self) -> None:
-        self._variable_inport_to_resource: Dict[InputPort, Tuple[Resource, int]] = {}
-        self._variable_outport_to_resource: Dict[OutputPort, Tuple[Resource, int]] = {}
-        self._operation_inport_to_resource: Dict[InputPort, Resource] = {}
-        self._operation_outport_to_resource: Dict[OutputPort, Resource] = {}
+        self._variable_inport_to_resource: DefaultDict[
+            InputPort, Set[Tuple[Resource, int]]
+        ] = defaultdict(set)
+        self._variable_outport_to_resource: DefaultDict[
+            OutputPort, Set[Tuple[Resource, int]]
+        ] = defaultdict(set)
+        self._operation_inport_to_resource = {}
+        self._operation_outport_to_resource = {}
         for pe in self.processing_elements:
             for operator in pe.processes:
                 for input_port in operator.operation.inputs:
@@ -527,19 +542,25 @@ of :class:`~b_asic.architecture.ProcessingElement`
         for memory in self.memories:
             for mv in memory:
                 for read_port in mv.read_ports:
-                    self._variable_inport_to_resource[read_port] = (memory, 0)  # Fix
-                self._variable_outport_to_resource[mv.write_port] = (memory, 0)  # Fix
+                    self._variable_inport_to_resource[read_port].add((memory, 0))  # Fix
+                self._variable_outport_to_resource[mv.write_port].add(
+                    (memory, 0)
+                )  # Fix
         if self._direct_interconnects:
             for di in self._direct_interconnects:
                 di = cast(MemoryVariable, di)
                 for read_port in di.read_ports:
-                    self._variable_inport_to_resource[read_port] = (
-                        self._operation_outport_to_resource[di.write_port],
-                        di.write_port.index,
+                    self._variable_inport_to_resource[read_port].add(
+                        (
+                            self._operation_outport_to_resource[di.write_port],
+                            di.write_port.index,
+                        )
                     )
-                    self._variable_outport_to_resource[di.write_port] = (
-                        self._operation_inport_to_resource[read_port],
-                        read_port.index,
+                    self._variable_outport_to_resource[di.write_port].add(
+                        (
+                            self._operation_inport_to_resource[read_port],
+                            read_port.index,
+                        )
                     )
 
     def validate_ports(self) -> None:
@@ -637,9 +658,11 @@ of :class:`~b_asic.architecture.ProcessingElement`
         for var in pe.collection:
             var = cast(OperatorProcess, var)
             for i, input_ in enumerate(var.operation.inputs):
-                d_in[i][self._variable_inport_to_resource[input_]] += 1
+                for v in self._variable_inport_to_resource[input_]:
+                    d_in[i][v] += 1
             for i, output in enumerate(var.operation.outputs):
-                d_out[i][self._variable_outport_to_resource[output]] += 1
+                for v in self._variable_outport_to_resource[output]:
+                    d_out[i][v] += 1
         return [dict(d) for d in d_in], [dict(d) for d in d_out]
 
     def resource_from_name(self, name: str):
@@ -688,8 +711,8 @@ of :class:`~b_asic.architecture.ProcessingElement`
             raise KeyError(f"{proc} not in {re_from.entity_name}")
         self._build_dicts()
 
-    def _digraph(self) -> Digraph:
-        edges: Set[Tuple[str, str, str]] = set()
+    def _digraph(self, branch_node=True) -> Digraph:
+        edges: DefaultDict[str, Set[Tuple[str, str]]] = defaultdict(set)
         dg = Digraph(node_attr={'shape': 'record'})
         for i, mem in enumerate(self._memories):
             dg.node(mem.entity_name, mem._struct_def())
@@ -699,24 +722,28 @@ of :class:`~b_asic.architecture.ProcessingElement`
             inputs, outputs = self.get_interconnects_for_pe(pe)
             for i, inp in enumerate(inputs):
                 for (source, port), cnt in inp.items():
-                    edges.add(
+                    edges[f"{source.entity_name}:out{port}"].add(
                         (
-                            f"{source.entity_name}:out{port}",
                             f"{pe.entity_name}:in{i}",
                             f"{cnt}",
                         )
                     )
             for o, output in enumerate(outputs):
                 for (dest, port), cnt in output.items():
-                    edges.add(
+                    edges[f"{pe.entity_name}:out{o}"].add(
                         (
-                            f"{pe.entity_name}:out{o}",
                             f"{dest.entity_name}:in{port}",
                             f"{cnt}",
                         )
                     )
-        for src_str, dest_str, cnt_str in edges:
-            dg.edge(src_str, dest_str, label=cnt_str)
+        for src_str, dest_cnts in edges.items():
+            if len(dest_cnts) > 1 and branch_node:
+                branch = f"{src_str}_branch".replace(":", "")
+                dg.node(branch, shape='point')
+                dg.edge(src_str, branch)
+                src_str = branch
+            for dest_str, cnt_str in dest_cnts:
+                dg.edge(src_str, dest_str, label=cnt_str)
         return dg
 
     @property
diff --git a/examples/folding_example_with_architecture.py b/examples/folding_example_with_architecture.py
index 5d531dd8..85a7fc65 100644
--- a/examples/folding_example_with_architecture.py
+++ b/examples/folding_example_with_architecture.py
@@ -107,25 +107,4 @@ arch = Architecture({p1, p2, p_in, p_out}, memories, direct_interconnects=direct
 
 # %%
 # The architecture can be rendered in enriched shells.
-#
-# .. graphviz::
-#
-#    digraph {
-#        node [shape=record]
-#        memory0 [label="{{<in0> in0}|memory0|{<out0> out0}}"]
-#        memory1 [label="{{<in0> in0}|memory1|{<out0> out0}}"]
-#        output [label="{{<in0> in0}|output}"]
-#        adder [label="{{<in0> in0|<in1> in1}|adder|{<out0> out0}}"]
-#        input [label="{input|{<out0> out0}}"]
-#        cmul [label="{{<in0> in0}|cmul|{<out0> out0}}"]
-#        memory1:out0 -> adder:in1 [label=2]
-#        cmul:out0 -> adder:in0 [label=2]
-#        memory0:out0 -> cmul:in0 [label=3]
-#        adder:out0 -> output:in0 [label=1]
-#        input:out0 -> adder:in0 [label=1]
-#        adder:out0 -> memory0:in0 [label=1]
-#        adder:out0 -> adder:in1 [label=2]
-#        memory0:out0 -> adder:in0 [label=1]
-#        adder:out0 -> cmul:in0 [label=1]
-#        cmul:out0 -> memory1:in0 [label=2]
-#    }
+arch
diff --git a/examples/secondorderdirectformiir_architecture.py b/examples/secondorderdirectformiir_architecture.py
index 82b50d2f..41b15843 100644
--- a/examples/secondorderdirectformiir_architecture.py
+++ b/examples/secondorderdirectformiir_architecture.py
@@ -91,28 +91,35 @@ arch = Architecture({p1, p2, p_in, p_out}, memories, direct_interconnects=direct
 
 # %%
 # The architecture can be rendered in enriched shells.
-#
-# .. graphviz::
-#
-#        digraph {
-#             node [shape=record]
-#             memory1 [label="{{<in0> in0}|memory1|{<out0> out0}}"]
-#             memory0 [label="{{<in0> in0}|memory0|{<out0> out0}}"]
-#             memory2 [label="{{<in0> in0}|memory2|{<out0> out0}}"]
-#             in [label="{in|{<out0> out0}}"]
-#             out [label="{{<in0> in0}|out}"]
-#             cmul [label="{{<in0> in0}|cmul|{<out0> out0}}"]
-#             adder [label="{{<in0> in0|<in1> in1}|adder|{<out0> out0}}"]
-#             memory1:out0 -> adder:in1 [label=1]
-#             cmul:out0 -> adder:in0 [label=1]
-#             cmul:out0 -> memory0:in0 [label=3]
-#             memory0:out0 -> adder:in0 [label=1]
-#             adder:out0 -> adder:in1 [label=1]
-#             memory1:out0 -> cmul:in0 [label=5]
-#             memory0:out0 -> adder:in1 [label=2]
-#             adder:out0 -> memory1:in0 [label=2]
-#             adder:out0 -> out:in0 [label=1]
-#             memory2:out0 -> adder:in0 [label=2]
-#             cmul:out0 -> memory2:in0 [label=2]
-#             in:out0 -> cmul:in0 [label=1]
-#        }
+arch
+
+# %%
+# To reduce the amount of interconnect, the ``cuml3.0`` variable can be moved from
+# ``memory0`` to ``memory2``.  In this way, ``memory0``only gets variables from the
+# adder and an input multiplexer can be avoided. The memoried must be assigned again as
+# the contents have changed.
+arch.move_process('cmul3.0', 'memory0', 'memory2')
+memories[0].assign()
+memories[2].assign()
+
+memories[0].show_content("New assigned memory0")
+memories[2].show_content("New assigned memory2")
+
+# %%
+# Looking at the architecture it is clear that there is now only one input to
+# ``memory0``, so no input multiplexer required.
+arch
+
+# %%
+# It is of course also possible to move ``add4.0`` to ``memory2`` to save one memory
+# cell.
+arch.move_process('add4.0', 'memory0', 'memory2')
+memories[0].assign()
+memories[2].assign()
+
+memories[0].show_content("New assigned memory0")
+memories[2].show_content("New assigned memory2")
+
+# %%
+# However, this comes at the expense of an additional input to ``memory2``.
+arch
-- 
GitLab