From 23f06d335c5fcf79a17182405a9cfb07bd971966 Mon Sep 17 00:00:00 2001
From: Simon Bjurek <simbj106@student.liu.se>
Date: Fri, 31 Jan 2025 20:04:00 +0100
Subject: [PATCH] added tests, did some bug fixes and added .coveragerc file
 for setting up excludes for code coverage

---
 .coveragerc                        |   4 +
 .gitignore                         |   1 +
 README.md                          |   6 +-
 b_asic/__init__.py                 |   1 +
 b_asic/scheduler.py                |  25 ++-
 pyproject.toml                     |   4 +
 test/fixtures/signal_flow_graph.py |   8 +
 test/test_scheduler.py             | 259 +++++++++++++++++++++++++++++
 8 files changed, 290 insertions(+), 18 deletions(-)
 create mode 100644 .coveragerc
 create mode 100644 test/test_scheduler.py

diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 00000000..a765f497
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,4 @@
+[report]
+exclude_lines =
+    .*if TYPE_CHECKING:.*
+    raise NotImplementedError
diff --git a/.gitignore b/.gitignore
index 8a416e12..4530b5b1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -117,3 +117,4 @@ b_asic/_version.py
 docs_sphinx/_build/
 docs_sphinx/examples
 result_images/
+.coverage
diff --git a/README.md b/README.md
index 074f2828..8f6a2984 100644
--- a/README.md
+++ b/README.md
@@ -24,11 +24,9 @@ The following packages are required in order to build the library:
   - [setuptools_scm](https://github.com/pypa/setuptools_scm/)
   - [NetworkX](https://networkx.org/)
   - [QtAwesome](https://github.com/spyder-ide/qtawesome/)
-- Qt 5 or 6, with Python bindings, one of:
-  - pyside2
-  - pyqt5
-  - pyside6
+- Qt 6, with Python bindings, one of:
   - pyqt6
+  - pyside6
 
 To build a binary distribution, the following additional packages are required:
 
diff --git a/b_asic/__init__.py b/b_asic/__init__.py
index fae7aec4..423e4676 100644
--- a/b_asic/__init__.py
+++ b/b_asic/__init__.py
@@ -9,6 +9,7 @@ from b_asic.operation import *
 from b_asic.port import *
 from b_asic.save_load_structure import *
 from b_asic.schedule import *
+from b_asic.scheduler import *
 from b_asic.signal import *
 from b_asic.signal_flow_graph import *
 from b_asic.simulation import *
diff --git a/b_asic/scheduler.py b/b_asic/scheduler.py
index cc4086d9..e109d01d 100644
--- a/b_asic/scheduler.py
+++ b/b_asic/scheduler.py
@@ -21,7 +21,7 @@ class Scheduler(ABC):
         schedule : Schedule
             Schedule to apply the scheduling algorithm on.
         """
-        pass
+        raise NotImplementedError
 
     def _handle_outputs(self, schedule, non_schedulable_ops=set()) -> None:
         for output in schedule.sfg.find_by_type_name(Output.type_name()):
@@ -77,14 +77,6 @@ class ASAPScheduler(Scheduler):
                 if operation.graph_id not in schedule.start_times:
                     op_start_time = 0
                     for current_input in operation.inputs:
-                        if len(current_input.signals) != 1:
-                            raise ValueError(
-                                "Error in scheduling, dangling input port detected."
-                            )
-                        if current_input.signals[0].source is None:
-                            raise ValueError(
-                                "Error in scheduling, signal with no source detected."
-                            )
                         source_port = current_input.signals[0].source
 
                         if source_port.operation.graph_id in non_schedulable_ops:
@@ -190,8 +182,8 @@ class EarliestDeadlineScheduler(Scheduler):
             schedule.move_operation_asap(input_op.graph_id)
 
         # construct the set of remaining operations, excluding inputs
-        remaining_ops = set(schedule.start_times.keys())
-        remaining_ops = {elem for elem in remaining_ops if not elem.startswith("in")}
+        remaining_ops = list(schedule.start_times.keys())
+        remaining_ops = [elem for elem in remaining_ops if not elem.startswith("in")]
 
         # construct a dictionarry for storing how many times until a resource is available again
         used_resources_ready_times = {}
@@ -218,9 +210,14 @@ class EarliestDeadlineScheduler(Scheduler):
             # if the resource is constrained, update remaining resources
             if best_candidate.type_name() in remaining_resources:
                 remaining_resources[best_candidate.type_name()] -= 1
-                used_resources_ready_times[best_candidate] = (
-                    current_time + best_candidate.execution_time
-                )
+                if best_candidate.execution_time:
+                    used_resources_ready_times[best_candidate] = (
+                        current_time + best_candidate.execution_time
+                    )
+                else:
+                    used_resources_ready_times[best_candidate] = (
+                        current_time + best_candidate.latency
+                    )
 
             # schedule the best candidate to the current time
             remaining_ops.remove(best_candidate.graph_id)
diff --git a/pyproject.toml b/pyproject.toml
index a9074708..bffec3c7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -30,6 +30,10 @@ classifiers = [
 ]
 dynamic = ["version", "authors"]
 
+[project.optional-dependencies]
+pyqt6 = ["pyqt6"]
+pyside6 = ["pyside6"]
+
 [tool.setuptools]
 zip-safe = false
 
diff --git a/test/fixtures/signal_flow_graph.py b/test/fixtures/signal_flow_graph.py
index 21c07d0f..7eaccbe7 100644
--- a/test/fixtures/signal_flow_graph.py
+++ b/test/fixtures/signal_flow_graph.py
@@ -332,3 +332,11 @@ def sfg_direct_form_iir_lp_filter():
     d1.input(0).connect(d0)
     y <<= a1 * d0 + a2 * d1 + a0 * top_node
     return SFG(inputs=[x], outputs=[y], name='Direct Form 2 IIR Lowpass filter')
+
+
+@pytest.fixture
+def sfg_empty():
+    """Empty SFG consisting of an Input followed by an Output."""
+    in0 = Input()
+    out0 = Output(in0)
+    return SFG(inputs=[in0], outputs=[out0])
diff --git a/test/test_scheduler.py b/test/test_scheduler.py
new file mode 100644
index 00000000..0959b2cb
--- /dev/null
+++ b/test/test_scheduler.py
@@ -0,0 +1,259 @@
+import pytest
+
+from b_asic.core_operations import Addition, ConstantMultiplication
+from b_asic.schedule import Schedule
+from b_asic.scheduler import ALAPScheduler, ASAPScheduler, EarliestDeadlineScheduler
+
+
+class TestASAPScheduler:
+    def test_empty_sfg(self, sfg_empty):
+        with pytest.raises(
+            ValueError, match="Empty signal flow graph cannot be scheduled."
+        ):
+            Schedule(sfg_empty, scheduler=ASAPScheduler())
+
+    def test_direct_form_2_iir(self, sfg_direct_form_iir_lp_filter):
+        sfg_direct_form_iir_lp_filter.set_latency_of_type(Addition.type_name(), 5)
+        sfg_direct_form_iir_lp_filter.set_latency_of_type(
+            ConstantMultiplication.type_name(), 4
+        )
+
+        schedule = Schedule(sfg_direct_form_iir_lp_filter, scheduler=ASAPScheduler())
+
+        assert schedule._start_times == {
+            "in0": 0,
+            "cmul1": 0,
+            "cmul4": 0,
+            "cmul2": 0,
+            "cmul3": 0,
+            "add3": 4,
+            "add1": 4,
+            "add0": 9,
+            "cmul0": 14,
+            "add2": 18,
+            "out0": 23,
+        }
+        assert schedule.schedule_time == 23
+
+    def test_direct_form_2_iir_with_scheduling_time(
+        self, sfg_direct_form_iir_lp_filter
+    ):
+        sfg_direct_form_iir_lp_filter.set_latency_of_type(Addition.type_name(), 5)
+        sfg_direct_form_iir_lp_filter.set_latency_of_type(
+            ConstantMultiplication.type_name(), 4
+        )
+
+        schedule = Schedule(
+            sfg_direct_form_iir_lp_filter, scheduler=ASAPScheduler(), schedule_time=30
+        )
+
+        assert schedule._start_times == {
+            "in0": 0,
+            "cmul1": 0,
+            "cmul4": 0,
+            "cmul2": 0,
+            "cmul3": 0,
+            "add3": 4,
+            "add1": 4,
+            "add0": 9,
+            "cmul0": 14,
+            "add2": 18,
+            "out0": 23,
+        }
+        assert schedule.schedule_time == 30
+
+
+class TestALAPScheduler:
+    def test_empty_sfg(self, sfg_empty):
+        with pytest.raises(
+            ValueError, match="Empty signal flow graph cannot be scheduled."
+        ):
+            Schedule(sfg_empty, scheduler=ALAPScheduler())
+
+    def test_direct_form_2_iir(self, sfg_direct_form_iir_lp_filter):
+        sfg_direct_form_iir_lp_filter.set_latency_of_type(Addition.type_name(), 5)
+        sfg_direct_form_iir_lp_filter.set_latency_of_type(
+            ConstantMultiplication.type_name(), 4
+        )
+
+        schedule = Schedule(sfg_direct_form_iir_lp_filter, scheduler=ALAPScheduler())
+
+        assert schedule._start_times == {
+            "cmul3": 0,
+            "cmul4": 0,
+            "add1": 4,
+            "in0": 9,
+            "cmul2": 9,
+            "cmul1": 9,
+            "add0": 9,
+            "add3": 13,
+            "cmul0": 14,
+            "add2": 18,
+            "out0": 23,
+        }
+        assert schedule.schedule_time == 23
+
+    def test_direct_form_2_iir_with_scheduling_time(
+        self, sfg_direct_form_iir_lp_filter
+    ):
+        sfg_direct_form_iir_lp_filter.set_latency_of_type(Addition.type_name(), 5)
+        sfg_direct_form_iir_lp_filter.set_latency_of_type(
+            ConstantMultiplication.type_name(), 4
+        )
+
+        schedule = Schedule(
+            sfg_direct_form_iir_lp_filter, scheduler=ALAPScheduler(), schedule_time=30
+        )
+
+        assert schedule._start_times == {
+            "cmul3": 7,
+            "cmul4": 7,
+            "add1": 11,
+            "in0": 16,
+            "cmul2": 16,
+            "cmul1": 16,
+            "add0": 16,
+            "add3": 20,
+            "cmul0": 21,
+            "add2": 25,
+            "out0": 30,
+        }
+        assert schedule.schedule_time == 30
+
+
+class TestEarliestDeadlineScheduler:
+    def test_empty_sfg(self, sfg_empty):
+        with pytest.raises(
+            ValueError, match="Empty signal flow graph cannot be scheduled."
+        ):
+            Schedule(sfg_empty, scheduler=EarliestDeadlineScheduler())
+
+    def test_direct_form_2_iir_inf_resources_no_exec_time(
+        self, sfg_direct_form_iir_lp_filter
+    ):
+        sfg_direct_form_iir_lp_filter.set_latency_of_type(Addition.type_name(), 5)
+        sfg_direct_form_iir_lp_filter.set_latency_of_type(
+            ConstantMultiplication.type_name(), 4
+        )
+
+        schedule = Schedule(
+            sfg_direct_form_iir_lp_filter, scheduler=EarliestDeadlineScheduler()
+        )
+
+        # should be the same as for ASAP due to infinite resources, except for input
+        assert schedule._start_times == {
+            "in0": 9,
+            "cmul1": 0,
+            "cmul4": 0,
+            "cmul2": 0,
+            "cmul3": 0,
+            "add3": 4,
+            "add1": 4,
+            "add0": 9,
+            "cmul0": 14,
+            "add2": 18,
+            "out0": 23,
+        }
+        assert schedule.schedule_time == 23
+
+    def test_direct_form_2_iir_1_add_1_mul_no_exec_time(
+        self, sfg_direct_form_iir_lp_filter
+    ):
+        sfg_direct_form_iir_lp_filter.set_latency_of_type(Addition.type_name(), 5)
+        sfg_direct_form_iir_lp_filter.set_latency_of_type(
+            ConstantMultiplication.type_name(), 4
+        )
+
+        max_resources = {ConstantMultiplication.type_name(): 1, Addition.type_name(): 1}
+
+        schedule = Schedule(
+            sfg_direct_form_iir_lp_filter,
+            scheduler=EarliestDeadlineScheduler(max_resources),
+        )
+        assert schedule._start_times == {
+            "cmul4": 0,
+            "cmul3": 4,
+            "cmul1": 8,
+            "add1": 8,
+            "cmul2": 12,
+            "in0": 13,
+            "add0": 13,
+            "add3": 18,
+            "cmul0": 18,
+            "add2": 23,
+            "out0": 28,
+        }
+
+        assert schedule.schedule_time == 28
+
+    def test_direct_form_2_iir_1_add_1_mul_exec_time_1(
+        self, sfg_direct_form_iir_lp_filter
+    ):
+        sfg_direct_form_iir_lp_filter.set_latency_of_type(
+            ConstantMultiplication.type_name(), 3
+        )
+        sfg_direct_form_iir_lp_filter.set_latency_of_type(Addition.type_name(), 2)
+        sfg_direct_form_iir_lp_filter.set_execution_time_of_type(
+            ConstantMultiplication.type_name(), 1
+        )
+        sfg_direct_form_iir_lp_filter.set_execution_time_of_type(
+            Addition.type_name(), 1
+        )
+
+        max_resources = {ConstantMultiplication.type_name(): 1, Addition.type_name(): 1}
+
+        schedule = Schedule(
+            sfg_direct_form_iir_lp_filter,
+            scheduler=EarliestDeadlineScheduler(max_resources),
+        )
+        assert schedule._start_times == {
+            "cmul4": 0,
+            "cmul3": 1,
+            "cmul1": 2,
+            "cmul2": 3,
+            "add1": 4,
+            "in0": 6,
+            "add0": 6,
+            "add3": 7,
+            "cmul0": 8,
+            "add2": 11,
+            "out0": 13,
+        }
+
+        assert schedule.schedule_time == 13
+
+    def test_direct_form_2_iir_2_add_3_mul_exec_time_1(
+        self, sfg_direct_form_iir_lp_filter
+    ):
+        sfg_direct_form_iir_lp_filter.set_latency_of_type(
+            ConstantMultiplication.type_name(), 3
+        )
+        sfg_direct_form_iir_lp_filter.set_latency_of_type(Addition.type_name(), 2)
+        sfg_direct_form_iir_lp_filter.set_execution_time_of_type(
+            ConstantMultiplication.type_name(), 1
+        )
+        sfg_direct_form_iir_lp_filter.set_execution_time_of_type(
+            Addition.type_name(), 1
+        )
+
+        max_resources = {ConstantMultiplication.type_name(): 3, Addition.type_name(): 2}
+
+        schedule = Schedule(
+            sfg_direct_form_iir_lp_filter,
+            scheduler=EarliestDeadlineScheduler(max_resources),
+        )
+        assert schedule._start_times == {
+            "cmul1": 0,
+            "cmul4": 0,
+            "cmul3": 0,
+            "cmul2": 1,
+            "add1": 3,
+            "add3": 4,
+            "in0": 5,
+            "add0": 5,
+            "cmul0": 7,
+            "add2": 10,
+            "out0": 12,
+        }
+
+        assert schedule.schedule_time == 12
-- 
GitLab