From 0cd0723a74b41e7537eed6deb1c1b0f643be7831 Mon Sep 17 00:00:00 2001
From: Mikael Henriksson <mikael.henriksson@liu.se>
Date: Thu, 16 Feb 2023 08:44:56 +0000
Subject: [PATCH] Process

---
 .gitignore                                    |   1 +
 README.md                                     |   1 +
 b_asic/process.py                             |  16 +-
 b_asic/resources.py                           | 343 ++++++++++++++++++
 docs_sphinx/api/index.rst                     |   1 +
 docs_sphinx/api/resources.rst                 |   7 +
 docs_sphinx/conf.py                           |   1 +
 pyproject.toml                                |   1 +
 requirements.txt                              |   1 +
 .../baseline/test_draw_process_collection.png | Bin 0 -> 10292 bytes
 test/conftest.py                              |   1 +
 test/fixtures/resources.py                    |  36 ++
 test/test_resources.py                        |  29 ++
 13 files changed, 437 insertions(+), 1 deletion(-)
 create mode 100644 b_asic/resources.py
 create mode 100644 docs_sphinx/api/resources.rst
 create mode 100644 test/baseline/test_draw_process_collection.png
 create mode 100644 test/fixtures/resources.py
 create mode 100644 test/test_resources.py

diff --git a/.gitignore b/.gitignore
index d251d2bf..d6034647 100644
--- a/.gitignore
+++ b/.gitignore
@@ -115,3 +115,4 @@ TODO.txt
 *.log
 b_asic/_version.py
 docs_sphinx/_build/
+docs_sphinx/examples
diff --git a/README.md b/README.md
index f6666690..f2df4a12 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,7 @@ The following packages are required in order to build the library:
     -   [NumPy](https://numpy.org/)
     -   [QtPy](https://github.com/spyder-ide/qtpy)
     -   [setuptools_scm](https://github.com/pypa/setuptools_scm/)
+    -   [NetworkX](https://networkx.org/)
 -   Qt 5 or 6, with Python bindings, one of:
     - pyside2
     - pyqt5
diff --git a/b_asic/process.py b/b_asic/process.py
index 131ad599..d48fa0ee 100644
--- a/b_asic/process.py
+++ b/b_asic/process.py
@@ -2,7 +2,7 @@
 B-ASIC classes representing resource usage.
 """
 
-from typing import Dict, Tuple
+from typing import Dict, Optional, Tuple
 
 from b_asic.operation import Operation
 from b_asic.port import InputPort, OutputPort
@@ -130,10 +130,14 @@ class PlainMemoryVariable(Process):
         write_time: int,
         write_port: int,
         reads: Dict[int, int],
+        name: Optional[str] = None,
     ):
         self._read_ports = tuple(reads.keys())
         self._life_times = tuple(reads.values())
         self._write_port = write_port
+        if name is None:
+            self._name = str(PlainMemoryVariable._name_cnt)
+            PlainMemoryVariable._name_cnt += 1
         super().__init__(
             start_time=write_time, execution_time=max(self._life_times)
         )
@@ -149,3 +153,13 @@ class PlainMemoryVariable(Process):
     @property
     def write_port(self) -> int:
         return self._write_port
+
+    @property
+    def name(self) -> str:
+        return self._name
+
+    def __str__(self) -> str:
+        return self._name
+
+    # Static counter for default names
+    _name_cnt = 0
diff --git a/b_asic/resources.py b/b_asic/resources.py
new file mode 100644
index 00000000..eaed928f
--- /dev/null
+++ b/b_asic/resources.py
@@ -0,0 +1,343 @@
+from typing import Dict, List, Optional, Set, Tuple, Union
+
+import matplotlib.pyplot as plt
+import networkx as nx
+from matplotlib.axes import Axes
+from matplotlib.ticker import MaxNLocator
+
+from b_asic.process import Process
+
+
+def draw_exclusion_graph_coloring(
+    exclusion_graph: nx.Graph,
+    color_dict: Dict[Process, int],
+    ax: Optional[Axes] = None,
+    color_list: Optional[
+        Union[List[str], List[Tuple[float, float, float]]]
+    ] = None,
+):
+    """
+    Use matplotlib.pyplot and networkx to draw a colored exclusion graph from the memory assigment
+
+    .. code-block:: python
+
+        _, ax = plt.subplots(1, 1)
+        collection = ProcessCollection(...)
+        exclusion_graph = collection.create_exclusion_graph_from_overlap()
+        color_dict = nx.greedy_color(exclusion_graph)
+        draw_exclusion_graph_coloring(exclusion_graph, color_dict, ax=ax[0])
+        plt.show()
+
+    Parameters
+    ----------
+    exclusion_graph : nx.Graph
+        A nx.Graph exclusion graph object that is to be drawn.
+
+    color_dict : dictionary
+        A color dictionary where keys are Process objects and where values are integers representing colors. These
+        dictionaries are automatically generated by :func:`networkx.algorithms.coloring.greedy_color`.
+
+    ax : :class:`matplotlib.axes.Axes`, optional
+        A Matplotlib Axes object to draw the exclusion graph
+
+    color_list : Optional[Union[List[str], List[Tuple[float,float,float]]]]
+    """
+    COLOR_LIST = [
+        '#aa0000',
+        '#00aa00',
+        '#0000ff',
+        '#ff00aa',
+        '#ffaa00',
+        '#00ffaa',
+        '#aaff00',
+        '#aa00ff',
+        '#00aaff',
+        '#ff0000',
+        '#00ff00',
+        '#0000aa',
+        '#aaaa00',
+        '#aa00aa',
+        '#00aaaa',
+    ]
+    node_color_dict = {}
+    if color_list is None:
+        node_color_dict = {k: COLOR_LIST[v] for k, v in color_dict.items()}
+    else:
+        node_color_dict = {k: color_list[v] for k, v in color_dict.items()}
+    node_color_list = [node_color_dict[node] for node in exclusion_graph]
+    nx.draw_networkx(
+        exclusion_graph,
+        node_color=node_color_list,
+        ax=ax,
+        pos=nx.spring_layout(exclusion_graph, seed=1),
+    )
+
+
+class ProcessCollection:
+    """
+    Collection of one or more processes
+
+    Parameters
+    ----------
+    collection : set of :class:`~b_asic.process.Process` objects, optional
+    """
+
+    def __init__(self, collection: Optional[Set[Process]] = None):
+        if collection is None:
+            self._collection: Set[Process] = set()
+        else:
+            self._collection = collection
+
+    def add_process(self, process: Process):
+        """
+        Add a new process to this process collection.
+
+        Parameters
+        ----------
+        process : Process
+            The process object to be added to the collection
+        """
+        self._collection.add(process)
+
+    def draw_lifetime_chart(
+        self,
+        schedule_time: int = 0,
+        ax: Optional[Axes] = None,
+        show_name: bool = True,
+    ):
+        """
+        Use matplotlib.pyplot to generate a process variable lifetime chart from this process collection.
+
+        Parameters
+        ----------
+        schedule_time : int, default: 0
+            Length of the time-axis in the generated graph. The time axis will span [0, schedule_time-1].
+            If set to zero (which is the default), the ...
+        ax : :class:`matplotlib.axes.Axes`, optional
+            Matplotlib Axes object to draw this lifetime chart onto. If not provided (i.e., set to None), this will
+            return a new axes object on return.
+        show_name : bool, default: True
+            Show name of all processes in the lifetime chart.
+
+        Returns
+        -------
+            ax: Associated Matplotlib Axes (or array of Axes) object
+        """
+
+        # Setup the Axes object
+        if ax is None:
+            _, _ax = plt.subplots()
+        else:
+            _ax = ax
+
+        # Draw the lifetime chart
+        PAD_L, PAD_R = 0.05, 0.05
+        max_execution_time = max(
+            [process.execution_time for process in self._collection]
+        )
+        schedule_time = (
+            schedule_time
+            if schedule_time
+            else max(p.start_time + p.execution_time for p in self._collection)
+        )
+        if max_execution_time > schedule_time:
+            # Schedule time needs to be greater than or equal to the maximum process life time
+            raise KeyError(
+                f'Error: Schedule time: {schedule_time} < Max execution time:'
+                f' {max_execution_time}'
+            )
+        for i, process in enumerate(
+            sorted(self._collection, key=lambda p: str(p))
+        ):
+            bar_start = process.start_time % schedule_time
+            bar_end = (
+                process.start_time + process.execution_time
+            ) % schedule_time
+            if bar_end > bar_start:
+                _ax.broken_barh(
+                    [(PAD_L + bar_start, bar_end - bar_start - PAD_L - PAD_R)],
+                    (i + 0.55, 0.9),
+                )
+            else:  # bar_end < bar_start
+                if bar_end != 0:
+                    _ax.broken_barh(
+                        [
+                            (
+                                PAD_L + bar_start,
+                                schedule_time - bar_start - PAD_L,
+                            )
+                        ],
+                        (i + 0.55, 0.9),
+                    )
+                    _ax.broken_barh([(0, bar_end - PAD_R)], (i + 0.55, 0.9))
+                else:
+                    _ax.broken_barh(
+                        [
+                            (
+                                PAD_L + bar_start,
+                                schedule_time - bar_start - PAD_L - PAD_R,
+                            )
+                        ],
+                        (i + 0.55, 0.9),
+                    )
+            if show_name:
+                _ax.annotate(
+                    str(process),
+                    (bar_start + PAD_L + 0.025, i + 1.00),
+                    va="center",
+                )
+        _ax.grid(True)
+
+        _ax.xaxis.set_major_locator(MaxNLocator(integer=True))
+        _ax.yaxis.set_major_locator(MaxNLocator(integer=True))
+        return _ax
+
+    def create_exclusion_graph_from_overlap(
+        self, add_name: bool = True
+    ) -> nx.Graph:
+        """
+        Generate exclusion graph based on processes overlaping in time
+
+        Parameters
+        ----------
+        add_name : bool, default: True
+            Add name of all processes as a node attribute in the exclusion graph.
+
+        Returns
+        -------
+            An nx.Graph exclusion graph where nodes are processes and arcs
+            between two processes indicated overlap in time
+        """
+        exclusion_graph = nx.Graph()
+        exclusion_graph.add_nodes_from(self._collection)
+        for process1 in self._collection:
+            for process2 in self._collection:
+                if process1 == process2:
+                    continue
+                else:
+                    t1 = set(
+                        range(
+                            process1.start_time,
+                            process1.start_time + process1.execution_time,
+                        )
+                    )
+                    t2 = set(
+                        range(
+                            process2.start_time,
+                            process2.start_time + process2.execution_time,
+                        )
+                    )
+                    if t1.intersection(t2):
+                        exclusion_graph.add_edge(process1, process2)
+        return exclusion_graph
+
+    def split(
+        self,
+        heuristic: str = "graph_color",
+        read_ports: Optional[int] = None,
+        write_ports: Optional[int] = None,
+        total_ports: Optional[int] = None,
+    ) -> Set["ProcessCollection"]:
+        """
+        Split this process storage based on some heuristic.
+
+        Parameters
+        ----------
+        heuristic : str, default: "graph_color"
+            The heuristic used when spliting this ProcessCollection.
+            Valid options are:
+                * "graph_color"
+                * "..."
+        read_ports : int, optional
+            The number of read ports used when spliting process collection based on memory variable access.
+        write_ports : int, optional
+            The number of write ports used when spliting process collection based on memory variable access.
+        total_ports : int, optional
+            The total number of ports used when spliting process collection based on memory variable access.
+
+        Returns
+        -------
+        A set of new ProcessColleciton objects with the process spliting.
+        """
+        if total_ports is None:
+            if read_ports is None or write_ports is None:
+                raise ValueError("inteligent quote")
+            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(
+                read_ports, write_ports, total_ports
+            )
+        else:
+            raise ValueError("Invalid heuristic provided")
+
+    def _split_graph_color(
+        self, read_ports: int, write_ports: int, total_ports: int
+    ) -> Set["ProcessCollection"]:
+        """
+        Parameters
+        ----------
+        read_ports : int, optional
+            The number of read ports used when spliting process collection based on memory variable access.
+        write_ports : int, optional
+            The number of write ports used when spliting process collection based on memory variable access.
+        total_ports : int, optional
+            The total number of ports used when spliting process collection based on memory variable access.
+        """
+        if read_ports != 1 or write_ports != 1:
+            raise ValueError(
+                "Spliting 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-concurent 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)
+
+        # 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
+        coloring = nx.coloring.greedy_color(exclusion_graph)
+        draw_exclusion_graph_coloring(exclusion_graph, coloring)
+        # process_collection_list = [ProcessCollection()]*(max(coloring.values()) + 1)
+        process_collection_list = [
+            ProcessCollection() for _ in range(max(coloring.values()) + 1)
+        ]
+        for process, color in coloring.items():
+            process_collection_list[color].add_process(process)
+        return {
+            process_collection
+            for process_collection in process_collection_list
+        }
diff --git a/docs_sphinx/api/index.rst b/docs_sphinx/api/index.rst
index 8ad0965a..c3480c10 100644
--- a/docs_sphinx/api/index.rst
+++ b/docs_sphinx/api/index.rst
@@ -10,6 +10,7 @@ API
     operation.rst
     port.rst
     process.rst
+    resources.rst
     schedule.rst
     sfg_generators.rst
     signal.rst
diff --git a/docs_sphinx/api/resources.rst b/docs_sphinx/api/resources.rst
new file mode 100644
index 00000000..d87fa73b
--- /dev/null
+++ b/docs_sphinx/api/resources.rst
@@ -0,0 +1,7 @@
+********************
+``b_asic.resources``
+********************
+
+.. automodule:: b_asic.resources
+   :members:
+   :undoc-members:
diff --git a/docs_sphinx/conf.py b/docs_sphinx/conf.py
index a8ac592d..90b049c7 100644
--- a/docs_sphinx/conf.py
+++ b/docs_sphinx/conf.py
@@ -39,6 +39,7 @@ intersphinx_mapping = {
     'matplotlib': ('https://matplotlib.org/stable/', None),
     'numpy': ('https://numpy.org/doc/stable/', None),
     'PyQt5': ("https://www.riverbankcomputing.com/static/Docs/PyQt5", None),
+    'networkx': ('https://networkx.org/documentation/stable', None),
 }
 
 numpydoc_show_class_members = False
diff --git a/pyproject.toml b/pyproject.toml
index fef65a9d..be37e866 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -13,6 +13,7 @@ dependencies = [
     "graphviz>=0.19",
     "matplotlib",
     "setuptools_scm[toml]>=6.2",
+    "networkx",
 ]
 classifiers = [
     "Intended Audience :: Education",
diff --git a/requirements.txt b/requirements.txt
index 34397383..1a591a91 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,3 +3,4 @@ qtpy
 graphviz>=0.19
 matplotlib
 setuptools_scm[toml]>=6.2
+networkx
diff --git a/test/baseline/test_draw_process_collection.png b/test/baseline/test_draw_process_collection.png
new file mode 100644
index 0000000000000000000000000000000000000000..0ab1996784015b6d0bc3e5bc68e0f3ae21c64d94
GIT binary patch
literal 10292
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

literal 0
HcmV?d00001

diff --git a/test/conftest.py b/test/conftest.py
index 179a82c2..138fefe0 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -2,6 +2,7 @@ import os
 from distutils import dir_util
 from test.fixtures.operation_tree import *
 from test.fixtures.port import *
+from test.fixtures.resources import *
 from test.fixtures.schedule import *
 from test.fixtures.signal import signal, signals
 from test.fixtures.signal_flow_graph import *
diff --git a/test/fixtures/resources.py b/test/fixtures/resources.py
new file mode 100644
index 00000000..6317e4cb
--- /dev/null
+++ b/test/fixtures/resources.py
@@ -0,0 +1,36 @@
+import pytest
+
+from b_asic.process import PlainMemoryVariable
+from b_asic.resources import ProcessCollection
+
+
+@pytest.fixture()
+def simple_collection():
+    NO_PORT = 0
+    return ProcessCollection(
+        {
+            PlainMemoryVariable(4, NO_PORT, {NO_PORT: 2}),
+            PlainMemoryVariable(2, NO_PORT, {NO_PORT: 6}),
+            PlainMemoryVariable(3, NO_PORT, {NO_PORT: 5}),
+            PlainMemoryVariable(6, NO_PORT, {NO_PORT: 2}),
+            PlainMemoryVariable(0, NO_PORT, {NO_PORT: 3}),
+            PlainMemoryVariable(0, NO_PORT, {NO_PORT: 2}),
+            PlainMemoryVariable(0, NO_PORT, {NO_PORT: 6}),
+        }
+    )
+
+
+@pytest.fixture()
+def collection():
+    NO_PORT = 0
+    return ProcessCollection(
+        {
+            PlainMemoryVariable(4, NO_PORT, {NO_PORT: 2}),
+            PlainMemoryVariable(2, NO_PORT, {NO_PORT: 6}),
+            PlainMemoryVariable(3, NO_PORT, {NO_PORT: 5}),
+            PlainMemoryVariable(6, NO_PORT, {NO_PORT: 2}),
+            PlainMemoryVariable(0, NO_PORT, {NO_PORT: 3}),
+            PlainMemoryVariable(0, NO_PORT, {NO_PORT: 2}),
+            PlainMemoryVariable(0, NO_PORT, {NO_PORT: 6}),
+        }
+    )
diff --git a/test/test_resources.py b/test/test_resources.py
new file mode 100644
index 00000000..67f20cc9
--- /dev/null
+++ b/test/test_resources.py
@@ -0,0 +1,29 @@
+import matplotlib.pyplot as plt
+import networkx as nx
+import pytest
+
+from b_asic.process import PlainMemoryVariable
+from b_asic.resources import ProcessCollection, draw_exclusion_graph_coloring
+
+
+class TestProcessCollectionPlainMemoryVariable:
+    @pytest.mark.mpl_image_compare(style='mpl20')
+    def test_draw_process_collection(self, simple_collection):
+        fig, ax = plt.subplots()
+        simple_collection.draw_lifetime_chart(ax=ax)
+        return fig
+
+    def test_draw_proces_collection(self, simple_collection):
+        _, ax = plt.subplots(1, 2)
+        simple_collection.draw_lifetime_chart(schedule_time=8, 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
-- 
GitLab