From dca62534671f75896df84a78d3eeeddded5f82aa Mon Sep 17 00:00:00 2001
From: Mikael Henriksson <mike.zx@hotmail.com>
Date: Fri, 17 Feb 2023 17:29:06 +0100
Subject: [PATCH] Add support for node selecting strategy in greedy_color of
 ProcessCollection spliting methods (closes #175)

---
 b_asic/resources.py                           | 230 +++++++++++++-----
 test/fixtures/interleaver-two-port-issue175.p | Bin 0 -> 2618 bytes
 test/test_resources.py                        |  40 +--
 3 files changed, 192 insertions(+), 78 deletions(-)
 create mode 100644 test/fixtures/interleaver-two-port-issue175.p

diff --git a/b_asic/resources.py b/b_asic/resources.py
index c50007a7..f6a0a1f0 100644
--- a/b_asic/resources.py
+++ b/b_asic/resources.py
@@ -187,10 +187,12 @@ class ProcessCollection:
         # Generate the life-time chart
         for i, process in enumerate(_sorted_nicely(self._collection)):
             bar_start = process.start_time % self._schedule_time
+            bar_end = process.start_time + process.execution_time
             bar_end = (
-                process.start_time + process.execution_time
-            ) % self._schedule_time
-            bar_end = self._schedule_time if bar_end == 0 else bar_end
+                bar_end
+                if bar_end == self._schedule_time
+                else bar_end % self._schedule_time
+            )
             if show_markers:
                 _ax.scatter(
                     x=bar_start,
@@ -240,16 +242,84 @@ class ProcessCollection:
         _ax.set_ylim(0.25, len(self._collection) + 0.75)
         return _ax
 
-    def create_exclusion_graph_from_overlap(
-        self, add_name: bool = True
+    def create_exclusion_graph_from_ports(
+        self,
+        read_ports: Optional[int] = None,
+        write_ports: Optional[int] = None,
+        total_ports: Optional[int] = None,
     ) -> nx.Graph:
         """
-        Generate exclusion graph based on processes overlapping in time
+        Create an exclusion graph from a ProcessCollection based on a number of read/write ports
 
         Parameters
         ----------
-        add_name : bool, default: True
-            Add name of all processes as a node attribute in the exclusion graph.
+        read_ports : int
+            The number of read ports used when splitting process collection based on memory variable access.
+        write_ports : int
+            The number of write ports used when splitting process collection based on memory variable access.
+        total_ports : int
+            The total number of ports used when splitting process collection based on memory variable access.
+
+        Returns
+        -------
+        nx.Graph
+
+        """
+        if total_ports is None:
+            if read_ports is None or write_ports is None:
+                raise ValueError(
+                    "If total_ports is unset, both read_ports and write_ports"
+                    " must be provided."
+                )
+            else:
+                total_ports = read_ports + write_ports
+        else:
+            read_ports = total_ports if read_ports is None else read_ports
+            write_ports = total_ports if write_ports is None else write_ports
+
+        # Guard for proper read/write port settings
+        if read_ports != 1 or write_ports != 1:
+            raise ValueError(
+                "Splitting with read and write ports not equal to one with the"
+                " graph coloring heuristic does not make sense."
+            )
+        if total_ports not in (1, 2):
+            raise ValueError(
+                "Total ports should be either 1 (non-concurrent reads/writes)"
+                " or 2 (concurrent read/writes) for graph coloring heuristic."
+            )
+
+        # Create new exclusion graph. Nodes are Processes
+        exclusion_graph = nx.Graph()
+        exclusion_graph.add_nodes_from(self._collection)
+        for node1 in exclusion_graph:
+            for node2 in exclusion_graph:
+                if node1 == node2:
+                    continue
+                else:
+                    node1_stop_time = node1.start_time + node1.execution_time
+                    node2_stop_time = node2.start_time + node2.execution_time
+                    if total_ports == 1:
+                        # Single-port assignment
+                        if node1.start_time == node2.start_time:
+                            exclusion_graph.add_edge(node1, node2)
+                        elif node1_stop_time == node2_stop_time:
+                            exclusion_graph.add_edge(node1, node2)
+                        elif node1.start_time == node2_stop_time:
+                            exclusion_graph.add_edge(node1, node2)
+                        elif node1_stop_time == node2.start_time:
+                            exclusion_graph.add_edge(node1, node2)
+                    else:
+                        # Dual-port assignment
+                        if node1.start_time == node2.start_time:
+                            exclusion_graph.add_edge(node1, node2)
+                        elif node1_stop_time == node2_stop_time:
+                            exclusion_graph.add_edge(node1, node2)
+        return exclusion_graph
+
+    def create_exclusion_graph_from_execution_time(self) -> nx.Graph:
+        """
+        Generate exclusion graph based on processes overlapping in time
 
         Returns
         -------
@@ -279,7 +349,47 @@ class ProcessCollection:
                         exclusion_graph.add_edge(process1, process2)
         return exclusion_graph
 
-    def split(
+    def split_execution_time(
+        self, heuristic: str = "graph_color", coloring_strategy: str = "DSATUR"
+    ) -> Set["ProcessCollection"]:
+        """
+        Split a ProcessCollection based on overlapping execution time.
+
+        Parameters
+        ----------
+        heuristic : str, default: 'graph_color'
+            The heuristic used when splitting based on execution times.
+            One of: 'graph_color', 'left_edge'.
+        coloring_strategy: str, default: 'DSATUR'
+            Node ordering strategy passed to nx.coloring.greedy_color() if the heuristic is set to 'graph_color'. This
+            parameter is only considered if heuristic is set to graph_color.
+            One of
+               * `'largest_first'`
+               * `'random_sequential'`
+               * `'smallest_last'`
+               * `'independent_set'`
+               * `'connected_sequential_bfs'`
+               * `'connected_sequential_dfs'`
+               * `'connected_sequential'` (alias for the previous strategy)
+               * `'saturation_largest_first'`
+               * `'DSATUR'` (alias for the saturation_largest_first strategy)
+
+        Returns
+        -------
+        A set of new ProcessCollection objects with the process splitting.
+        """
+        if heuristic == "graph_color":
+            exclusion_graph = self.create_exclusion_graph_from_execution_time()
+            coloring = nx.coloring.greedy_color(
+                exclusion_graph, strategy=coloring_strategy
+            )
+            return self._split_from_graph_coloring(coloring)
+        elif heuristic == "left_edge":
+            raise NotImplementedError()
+        else:
+            raise ValueError(f"Invalid heuristic '{heuristic}'")
+
+    def split_ports(
         self,
         heuristic: str = "graph_color",
         read_ports: Optional[int] = None,
@@ -309,77 +419,79 @@ class ProcessCollection:
         """
         if total_ports is None:
             if read_ports is None or write_ports is None:
-                raise ValueError("inteligent quote")
+                raise ValueError(
+                    "If total_ports is unset, both read_ports and write_ports"
+                    " must be provided."
+                )
             else:
                 total_ports = read_ports + write_ports
         else:
             read_ports = total_ports if read_ports is None else read_ports
             write_ports = total_ports if write_ports is None else write_ports
-
         if heuristic == "graph_color":
-            return self._split_graph_color(
+            return self._split_ports_graph_color(
                 read_ports, write_ports, total_ports
             )
         else:
-            raise ValueError("Invalid heuristic provided")
+            raise ValueError("Invalid heuristic provided.")
 
-    def _split_graph_color(
-        self, read_ports: int, write_ports: int, total_ports: int
+    def _split_ports_graph_color(
+        self,
+        read_ports: int,
+        write_ports: int,
+        total_ports: int,
+        coloring_strategy: str = "DSATUR",
     ) -> Set["ProcessCollection"]:
         """
         Parameters
         ----------
-        read_ports : int, optional
+        read_ports : int
             The number of read ports used when splitting process collection based on memory variable access.
-        write_ports : int, optional
+        write_ports : int
             The number of write ports used when splitting process collection based on memory variable access.
-        total_ports : int, optional
+        total_ports : int
             The total number of ports used when splitting process collection based on memory variable access.
+        coloring_strategy: str, default: 'DSATUR'
+            Node ordering strategy passed to nx.coloring.greedy_color()
+            One of
+               * `'largest_first'`
+               * `'random_sequential'`
+               * `'smallest_last'`
+               * `'independent_set'`
+               * `'connected_sequential_bfs'`
+               * `'connected_sequential_dfs'`
+               * `'connected_sequential'` (alias for the previous strategy)
+               * `'saturation_largest_first'`
+               * `'DSATUR'` (alias for the saturation_largest_first strategy)
         """
-        if read_ports != 1 or write_ports != 1:
-            raise ValueError(
-                "Splitting with read and write ports not equal to one with the"
-                " graph coloring heuristic does not make sense."
-            )
-        if total_ports not in (1, 2):
-            raise ValueError(
-                "Total ports should be either 1 (non-concurrent reads/writes)"
-                " or 2 (concurrent read/writes) for graph coloring heuristic."
-            )
-
         # Create new exclusion graph. Nodes are Processes
-        exclusion_graph = nx.Graph()
-        exclusion_graph.add_nodes_from(self._collection)
+        exclusion_graph = self.create_exclusion_graph_from_ports(
+            read_ports, write_ports, total_ports
+        )
 
-        # Add exclusions (arcs) between processes in the exclusion graph
-        for node1 in exclusion_graph:
-            for node2 in exclusion_graph:
-                if node1 == node2:
-                    continue
-                else:
-                    node1_stop_time = node1.start_time + node1.execution_time
-                    node2_stop_time = node2.start_time + node2.execution_time
-                    if total_ports == 1:
-                        # Single-port assignment
-                        if node1.start_time == node2.start_time:
-                            exclusion_graph.add_edge(node1, node2)
-                        elif node1_stop_time == node2_stop_time:
-                            exclusion_graph.add_edge(node1, node2)
-                        elif node1.start_time == node2_stop_time:
-                            exclusion_graph.add_edge(node1, node2)
-                        elif node1_stop_time == node2.start_time:
-                            exclusion_graph.add_edge(node1, node2)
-                    else:
-                        # Dual-port assignment
-                        if node1.start_time == node2.start_time:
-                            exclusion_graph.add_edge(node1, node2)
-                        elif node1_stop_time == node2_stop_time:
-                            exclusion_graph.add_edge(node1, node2)
+        # Perform assignment from coloring and return result
+        coloring = nx.coloring.greedy_color(
+            exclusion_graph, strategy=coloring_strategy
+        )
+        return self._split_from_graph_coloring(coloring)
+
+    def _split_from_graph_coloring(
+        self,
+        coloring: Dict[Process, int],
+    ) -> Set["ProcessCollection"]:
+        """
+        Split :class:`Process` objects into a set of :class:`ProcessesCollection` objects based on a provided graph coloring.
+        Resulting :class:`ProcessCollection` will have the same schedule time and cyclic propoery as self.
+
+        Parameters
+        ----------
+        coloring : Dict[Process, int]
+            Process->int (color) mappings
 
-        # Perform assignment
-        coloring = nx.coloring.greedy_color(exclusion_graph)
-        draw_exclusion_graph_coloring(exclusion_graph, coloring)
-        # process_collection_list = [ProcessCollection()]*(max(coloring.values()) + 1)
+        Returns
+        -------
+        A set of new ProcessCollections.
+        """
         process_collection_set_list = [
             set() for _ in range(max(coloring.values()) + 1)
         ]
diff --git a/test/fixtures/interleaver-two-port-issue175.p b/test/fixtures/interleaver-two-port-issue175.p
new file mode 100644
index 0000000000000000000000000000000000000000..f62ee38cbca0f1ec5ff14ddc073b58976cb69ad1
GIT binary patch
literal 2618
zcmai$OK;jx5Jn*-1wtSYpr!9XUr8Ut4!1U5cWG71s$0uAu58KR$Tn)TsMIbhbz!NM
zdi#GnJalKUMk))0tmE-_XU<rBtNcAbD6Kzvd*--l=#3LUjhBh%r!wzcCNca--^Wqp
zds!GS<R5wVU4E6PdBgGW>Myv^+TMS?xku*R%g7BEAN+Zo+<bJC(49p-y+85YE9W{+
zvQ*lo9}+4?;V0k8!Z|$K9<Hx^PQuJzL%;x$W^R(L5otflj~xGt?=4rsw%3OFzO!)W
zzRc^Z?BkP(CFF7z)HkVv!_||5mfeBxwp|K3_QT-V?gU-rCmcUgd<pmw;f;43-&g#7
z;QNHPbdJY74~ls%I3DYl3-t|y<1zoeLVio*_`1qJ0KQK0Pc4q$Q~X`v_b5N*c&vY?
zSl{G$?9VOW>AVcKenaKy1K*%|*#49i-vho(d3L^4#dm?PQvN@_9T)OXS>G^Er<jNJ
z0k&o9a|QSz$z!ndT5Z9`|2FU?!cSR0ab7y`#0R0X=S3fmfZy?f^=+W?>;pd_c}$ka
z`r2-Np|kzO`i~3sEq1<5m8S)KljdRjiM};~r@oo2pP1)KF%Rn}_WvCCo&K{vV4m@A
z9-*`I#XN_FJSMx3*#8FbbiO8=AANocJlQXyv-8FMdILP!FQKvh$M?f);K}<zXl(y6
z&#OWnjXf{!%Ng)wUxdb<7yEMxJn4_nSl=qjw*%lS)HjxIDZUPTi}LI~qR%gZCq4^}
z<$J1r4fr0d&+Zrc_5yg~ztC77eR~c(*>|C_K4U+h0l(8v*5{haUj@EK^RxBQpFQA-
zKNExX0q6A;crq`cu|DIxM!?f~S**|4pWDEb{!C2vyg08R@N`}l>p%AA1bEsXYjxh=
ddCN(?z`t5Xn?GdIzR0VNcjHB&Cx0$y<9~OV7z6+S

literal 0
HcmV?d00001

diff --git a/test/test_resources.py b/test/test_resources.py
index 38dfc245..581474af 100644
--- a/test/test_resources.py
+++ b/test/test_resources.py
@@ -1,3 +1,5 @@
+import pickle
+
 import matplotlib.pyplot as plt
 import networkx as nx
 import pytest
@@ -6,7 +8,7 @@ from b_asic.research.interleaver import (
     generate_matrix_transposer,
     generate_random_interleaver,
 )
-from b_asic.resources import draw_exclusion_graph_coloring
+from b_asic.resources import ProcessCollection, draw_exclusion_graph_coloring
 
 
 class TestProcessCollectionPlainMemoryVariable:
@@ -16,40 +18,40 @@ class TestProcessCollectionPlainMemoryVariable:
         simple_collection.draw_lifetime_chart(ax=ax, show_markers=False)
         return fig
 
-    def test_draw_proces_collection(self, simple_collection):
-        _, ax = plt.subplots(1, 2)
-        simple_collection.draw_lifetime_chart(ax=ax[0])
-        exclusion_graph = (
-            simple_collection.create_exclusion_graph_from_overlap()
-        )
-        color_dict = nx.coloring.greedy_color(exclusion_graph)
-        draw_exclusion_graph_coloring(exclusion_graph, color_dict, ax=ax[1])
-
-    def test_split_memory_variable(self, simple_collection):
-        collection_split = simple_collection.split(
-            read_ports=1, write_ports=1, total_ports=2
-        )
-        assert len(collection_split) == 3
-
     @pytest.mark.mpl_image_compare(style='mpl20')
     def test_draw_matrix_transposer_4(self):
         fig, ax = plt.subplots()
         generate_matrix_transposer(4).draw_lifetime_chart(ax=ax)
         return fig
 
+    def test_split_memory_variable(self, simple_collection: ProcessCollection):
+        collection_split = simple_collection.split_ports(
+            heuristic="graph_color", read_ports=1, write_ports=1, total_ports=2
+        )
+        assert len(collection_split) == 3
+
+    # Issue: #175
+    def test_interleaver_issue175(self):
+        with open('test/fixtures/interleaver-two-port-issue175.p', 'rb') as f:
+            interleaver_collection: ProcessCollection = pickle.load(f)
+            assert len(interleaver_collection.split_ports(total_ports=1)) == 2
+
     def test_generate_random_interleaver(self):
-        return
         for _ in range(10):
             for size in range(5, 20, 5):
                 assert (
                     len(
-                        generate_random_interleaver(size).split(
+                        generate_random_interleaver(size).split_ports(
                             read_ports=1, write_ports=1
                         )
                     )
                     == 1
                 )
                 assert (
-                    len(generate_random_interleaver(size).split(total_ports=1))
+                    len(
+                        generate_random_interleaver(size).split_ports(
+                            total_ports=1
+                        )
+                    )
                     == 2
                 )
-- 
GitLab