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