diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4a822db9af4dcb4c6ef6615ad8caf6e22d2e4728..7accae47772177650d8e549fc452c933a76ff6b4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,7 +4,7 @@ stages: before_script: - apt-get update --yes - # - apt-get install --yes build-essential cmake graphviz python3-pyqt5 xvfb xdg-utils lcov + # - apt-get install --yes build-essential cmake graphviz xvfb xdg-utils lcov - apt-get install --yes graphviz python3-pyqt5 xvfb xdg-utils - apt-get install -y libxcb-cursor-dev - python -m pip install --upgrade pip diff --git a/b_asic/gui_utils/mpl_window.py b/b_asic/gui_utils/mpl_window.py index 266d4ae7669a521ca2be9f211fc20c7fb50dc476..1a6db6785c5dcf473b767a23adf4b00994d6c2bd 100644 --- a/b_asic/gui_utils/mpl_window.py +++ b/b_asic/gui_utils/mpl_window.py @@ -1,7 +1,7 @@ """MPLWindow is a dialog that provides an Axes for plotting in.""" -from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas -from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar +from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.backends.backend_qtagg import NavigationToolbar2QT as NavigationToolbar from matplotlib.figure import Figure from qtpy.QtCore import Qt from qtpy.QtWidgets import QDialog, QVBoxLayout diff --git a/b_asic/logger.py b/b_asic/logger.py index e6615ed0f8986779b272b078256d451877aa8fc1..606ac6f975a2084f779ebc5e23cef4c454453136 100644 --- a/b_asic/logger.py +++ b/b_asic/logger.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -B-ASIC Scheduler-gui Logger Module. +B-ASIC Logger Module. Contains a logger that logs to the console and a file using levels. It is based on the :mod:`logging` module and has predefined levels of logging. @@ -8,7 +8,7 @@ on the :mod:`logging` module and has predefined levels of logging. Usage: ------ - >>> import b_asic.scheduler_gui.logger as logger + >>> import b_asic.logger as logger >>> log = logger.getLogger() >>> log.info('This is a log post with level INFO') diff --git a/b_asic/schedule.py b/b_asic/schedule.py index 181c426ef8a680257a8bebff042adf9386d99e88..543e10af0612ddbef2d20277b6d4d47db59b582c 100644 --- a/b_asic/schedule.py +++ b/b_asic/schedule.py @@ -117,10 +117,13 @@ class Schedule: self._start_times = start_times self._laps.update(laps) self._remove_delays_no_laps() + max_end_time = self.get_max_end_time() if not self._schedule_time: self._schedule_time = max_end_time + self._validate_schedule() + def __str__(self) -> str: """Return a string representation of this Schedule.""" res: List[Tuple[GraphID, int, int, int]] = [ @@ -155,6 +158,32 @@ class Schedule: return string_io.getvalue() + def _validate_schedule(self) -> None: + if self._schedule_time is None: + raise ValueError("Schedule without set scheduling time detected.") + if not isinstance(self._schedule_time, int): + raise ValueError("Schedule with non-integer scheduling time detected.") + + ops = {op.graph_id for op in self._sfg.operations} + missing_elems = ops - set(self._start_times) + extra_elems = set(self._start_times) - ops + if missing_elems: + raise ValueError( + f"Missing operations detected in start_times: {missing_elems}" + ) + if extra_elems: + raise ValueError(f"Extra operations detected in start_times: {extra_elems}") + + for graph_id, time in self._start_times.items(): + if self.forward_slack(graph_id) < 0 or self.backward_slack(graph_id) < 0: + raise ValueError( + f"Negative slack detected in Schedule for operation: {graph_id}." + ) + if time > self._schedule_time: + raise ValueError( + f"Start time larger than scheduling time detected in Schedule for operation {graph_id}" + ) + def start_time_of_operation(self, graph_id: GraphID) -> int: """ Return the start time of the operation with the specified by *graph_id*. @@ -1122,7 +1151,7 @@ class Schedule: color="black", ) - def _reset_y_locations(self) -> None: + def reset_y_locations(self) -> None: """Reset all the y-locations in the schedule to None""" self._y_locations = defaultdict(_y_locations_default) diff --git a/b_asic/scheduler.py b/b_asic/scheduler.py index 74a892c042699f9bcb3beac5d4d6e506c9795d1c..87708e7527bd05c42baa487d5ce934f35ddd7c3a 100644 --- a/b_asic/scheduler.py +++ b/b_asic/scheduler.py @@ -172,6 +172,7 @@ class ListScheduler(Scheduler, ABC): cyclic: Optional[bool] = False, ) -> None: super() + if max_resources is not None: if not isinstance(max_resources, dict): raise ValueError("max_resources must be a dictionary.") diff --git a/b_asic/scheduler_gui/compile.py b/b_asic/scheduler_gui/compile.py index 2dc7b719d7fdff9fa8b5f7fd0530814eb6f21b75..fe97bf54fe2c71d50266a8bee4e6d06610122556 100644 --- a/b_asic/scheduler_gui/compile.py +++ b/b_asic/scheduler_gui/compile.py @@ -18,7 +18,7 @@ from qtpy import uic from setuptools_scm import get_version try: - import b_asic.scheduler_gui.logger as logger + import b_asic.logger as logger log = logger.getLogger() sys.excepthook = logger.handle_exceptions @@ -37,18 +37,16 @@ def _check_filenames(*filenames: str) -> None: def _check_qt_version() -> None: """ - Check if PySide2, PyQt5, PySide6, or PyQt6 is installed. + Check if PySide6 or PyQt6 is installed. Otherwise, raise AssertionError exception. """ - assert ( - uic.PYSIDE2 or uic.PYQT5 or uic.PYSIDE6 or uic.PYQT6 - ), "Python QT bindings must be installed" + assert uic.PYSIDE6 or uic.PYQT6, "Python QT bindings must be installed" def replace_qt_bindings(filename: str) -> None: """ - Replace qt-binding API in *filename* from PySide2/6 or PyQt5/6 to qtpy. + Replace qt-binding API in *filename* from PySide6 or PyQt6 to qtpy. Parameters ---------- @@ -57,8 +55,6 @@ def replace_qt_bindings(filename: str) -> None: """ with open(f"{filename}") as file: filedata = file.read() - filedata = filedata.replace("from PyQt5", "from qtpy") - filedata = filedata.replace("from PySide2", "from qtpy") filedata = filedata.replace("from PyQt6", "from qtpy") filedata = filedata.replace("from PySide6", "from qtpy") with open(f"{filename}", "w") as file: diff --git a/b_asic/scheduler_gui/main_window.py b/b_asic/scheduler_gui/main_window.py index c15be2679acd4a0e725638dc8849d2ae4ba83000..217afa8a55dda4d3c96f0cf1fbfeb499332958fd 100644 --- a/b_asic/scheduler_gui/main_window.py +++ b/b_asic/scheduler_gui/main_window.py @@ -56,7 +56,7 @@ from qtpy.QtWidgets import ( ) # B-ASIC -import b_asic.scheduler_gui.logger as logger +import b_asic.logger as logger from b_asic._version import __version__ from b_asic.graph_component import GraphComponent, GraphID from b_asic.gui_utils.about_window import AboutWindow diff --git a/b_asic/scheduler_gui/scheduler_event.py b/b_asic/scheduler_gui/scheduler_event.py index 89d4a3848c9967418bf819dc7f48b396b4995bae..617e14cb73e39d0a0c1a62076e4d0f6bc82b4592 100644 --- a/b_asic/scheduler_gui/scheduler_event.py +++ b/b_asic/scheduler_gui/scheduler_event.py @@ -19,7 +19,7 @@ from b_asic.scheduler_gui.operation_item import OperationItem from b_asic.scheduler_gui.timeline_item import TimelineItem -class SchedulerEvent: # PyQt5 +class SchedulerEvent: """ Event filter and handlers for SchedulerItem. @@ -29,7 +29,7 @@ class SchedulerEvent: # PyQt5 The parent QGraphicsItem. """ - class Signals(QObject): # PyQt5 + class Signals(QObject): """A class representing signals.""" component_selected = Signal(str) @@ -43,11 +43,11 @@ class SchedulerEvent: # PyQt5 _axes: Optional[AxesItem] _current_pos: QPointF _delta_time: int - _signals: Signals # PyQt5 + _signals: Signals _schedule: Schedule _old_op_position: int = -1 - def __init__(self, parent: Optional[QGraphicsItem] = None): # PyQt5 + def __init__(self, parent: Optional[QGraphicsItem] = None): super().__init__(parent=parent) self._signals = self.Signals() diff --git a/b_asic/scheduler_gui/scheduler_item.py b/b_asic/scheduler_gui/scheduler_item.py index 80e09ac6d3f9d696e62e9f2124b26435ba3e9042..13af04a9cc22f7de7a032591798089e492d8eff9 100644 --- a/b_asic/scheduler_gui/scheduler_item.py +++ b/b_asic/scheduler_gui/scheduler_item.py @@ -31,7 +31,7 @@ from b_asic.scheduler_gui.signal_item import SignalItem from b_asic.types import GraphID -class SchedulerItem(SchedulerEvent, QGraphicsItemGroup): # PySide2 / PyQt5 +class SchedulerItem(SchedulerEvent, QGraphicsItemGroup): """ A class to represent a schedule in a QGraphicsScene. @@ -312,7 +312,7 @@ class SchedulerItem(SchedulerEvent, QGraphicsItemGroup): # PySide2 / PyQt5 ) def _redraw_from_start(self) -> None: - self.schedule._reset_y_locations() + self.schedule.reset_y_locations() self.schedule.sort_y_locations_on_start_times() for graph_id in self.schedule.start_times.keys(): self._set_position(graph_id) diff --git a/docs_sphinx/conf.py b/docs_sphinx/conf.py index ed0e0e06f467cae956415a5bb8b9cb45be311145..32ba20282f013e59b0d0bf5be04fe24687efddf9 100644 --- a/docs_sphinx/conf.py +++ b/docs_sphinx/conf.py @@ -40,7 +40,6 @@ intersphinx_mapping = { 'graphviz': ('https://graphviz.readthedocs.io/en/stable/', None), '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), 'mplsignal': ('https://mplsignal.readthedocs.io/en/stable/', None), } diff --git a/docs_sphinx/index.rst b/docs_sphinx/index.rst index bcbb9c1e08c0de3c2b6e5140f5278026b34be52e..6cb1b85f6f94d854de73a74ba3c2aa4e58486aa0 100644 --- a/docs_sphinx/index.rst +++ b/docs_sphinx/index.rst @@ -39,7 +39,7 @@ can pull new changes without having to reinstall it. It also makes it easy to co any improvements. In addition to the dependencies that are automatically installed, you will also -need a Qt-binding, but you are free to choose from the available Qt5 and Qt6 bindings. +need a Qt-binding, but you are free to choose between PyQt6 and PySide6. See `https://gitlab.liu.se/da/B-ASIC <https://gitlab.liu.se/da/B-ASIC>`_ for more info. If you use B-ASIC in a publication, please acknowledge it. Later on there will be a diff --git a/test/unit/test_schedule.py b/test/unit/test_schedule.py index 4d45755d3e7498b77b3ba89d77e13b4c09a55368..e7c51569764277edde7162f49d407bee5bbb93bc 100644 --- a/test/unit/test_schedule.py +++ b/test/unit/test_schedule.py @@ -11,7 +11,7 @@ from b_asic.core_operations import Addition, Butterfly, ConstantMultiplication from b_asic.process import OperatorProcess from b_asic.schedule import Schedule from b_asic.scheduler import ALAPScheduler, ASAPScheduler -from b_asic.sfg_generators import direct_form_fir +from b_asic.sfg_generators import direct_form_1_iir, direct_form_fir from b_asic.signal_flow_graph import SFG from b_asic.special_operations import Delay, Input, Output @@ -247,6 +247,50 @@ class TestInit: } assert schedule.schedule_time == 10 + def test_provided_schedule(self): + sfg = direct_form_1_iir([1, 2, 3], [1, 2, 3]) + + sfg.set_latency_of_type(Addition.type_name(), 1) + sfg.set_latency_of_type(ConstantMultiplication.type_name(), 3) + sfg.set_execution_time_of_type(Addition.type_name(), 1) + sfg.set_execution_time_of_type(ConstantMultiplication.type_name(), 1) + + start_times = { + "in0": 1, + "cmul0": 1, + "cmul1": 0, + "cmul2": 0, + "cmul3": 0, + "cmul4": 0, + "add3": 3, + "add1": 3, + "add0": 4, + "add2": 5, + "out0": 6, + } + laps = { + 's8': 1, + 's10': 2, + 's15': 1, + 's17': 2, + 's0': 0, + 's3': 0, + 's12': 0, + 's11': 0, + 's14': 0, + 's13': 0, + 's6': 0, + 's4': 0, + 's5': 0, + 's2': 0, + } + + schedule = Schedule(sfg, start_times=start_times, laps=laps) + + assert schedule.start_times == start_times + assert schedule.laps == laps + assert schedule.schedule_time == 6 + class TestSlacks: def test_forward_backward_slack_normal_latency(self, precedence_sfg_delays): @@ -297,24 +341,22 @@ class TestSlacks: schedule = Schedule(precedence_sfg_delays, scheduler=ASAPScheduler()) schedule.print_slacks() captured = capsys.readouterr() - assert ( - captured.out - == """Graph ID | Backward | Forward ----------|----------|--------- -add0 | 0 | 0 -add1 | 0 | 0 -add2 | 0 | 0 -add3 | 0 | 7 -cmul0 | 0 | 1 -cmul1 | 0 | 0 -cmul2 | 0 | 0 -cmul3 | 4 | 0 -cmul4 | 16 | 0 -cmul5 | 16 | 0 -cmul6 | 4 | 0 -in0 | oo | 0 -out0 | 0 | oo -""" + assert captured.out == ( + "Graph ID | Backward | Forward\n" + "---------|----------|---------\n" + "add0 | 0 | 0\n" + "add1 | 0 | 0\n" + "add2 | 0 | 0\n" + "add3 | 0 | 7\n" + "cmul0 | 0 | 1\n" + "cmul1 | 0 | 0\n" + "cmul2 | 0 | 0\n" + "cmul3 | 4 | 0\n" + "cmul4 | 16 | 0\n" + "cmul5 | 16 | 0\n" + "cmul6 | 4 | 0\n" + "in0 | oo | 0\n" + "out0 | 0 | oo\n" ) assert captured.err == "" @@ -325,24 +367,22 @@ out0 | 0 | oo schedule = Schedule(precedence_sfg_delays, scheduler=ASAPScheduler()) schedule.print_slacks(1) captured = capsys.readouterr() - assert ( - captured.out - == """Graph ID | Backward | Forward ----------|----------|--------- -cmul0 | 0 | 1 -add0 | 0 | 0 -add1 | 0 | 0 -cmul1 | 0 | 0 -cmul2 | 0 | 0 -add3 | 0 | 7 -add2 | 0 | 0 -out0 | 0 | oo -cmul3 | 4 | 0 -cmul6 | 4 | 0 -cmul4 | 16 | 0 -cmul5 | 16 | 0 -in0 | oo | 0 -""" + assert captured.out == ( + "Graph ID | Backward | Forward\n" + "---------|----------|---------\n" + "cmul0 | 0 | 1\n" + "add0 | 0 | 0\n" + "add1 | 0 | 0\n" + "cmul1 | 0 | 0\n" + "cmul2 | 0 | 0\n" + "add3 | 0 | 7\n" + "add2 | 0 | 0\n" + "out0 | 0 | oo\n" + "cmul3 | 4 | 0\n" + "cmul6 | 4 | 0\n" + "cmul4 | 16 | 0\n" + "cmul5 | 16 | 0\n" + "in0 | oo | 0\n" ) assert captured.err == "" @@ -802,10 +842,23 @@ class TestYLocations: sfg_simple_filter.set_latency_of_type(Addition.type_name(), 1) sfg_simple_filter.set_latency_of_type(ConstantMultiplication.type_name(), 2) schedule = Schedule(sfg_simple_filter, ASAPScheduler()) - # Assign locations - schedule.show() - assert schedule._y_locations == {'in0': 0, 'cmul0': 1, 'add0': 3, 'out0': 2} - schedule.move_y_location('add0', 1, insert=True) - assert schedule._y_locations == {'in0': 0, 'cmul0': 2, 'add0': 1, 'out0': 3} - schedule.move_y_location('out0', 1) - assert schedule._y_locations == {'in0': 0, 'cmul0': 2, 'add0': 1, 'out0': 1} + + assert schedule._y_locations == {"in0": 0, "cmul0": 1, "add0": 3, "out0": 2} + schedule.move_y_location("add0", 1, insert=True) + assert schedule._y_locations == {"in0": 0, "cmul0": 2, "add0": 1, "out0": 3} + schedule.move_y_location("out0", 1) + assert schedule._y_locations == {"in0": 0, "cmul0": 2, "add0": 1, "out0": 1} + + def test_reset(self, sfg_simple_filter): + sfg_simple_filter.set_latency_of_type(Addition.type_name(), 1) + sfg_simple_filter.set_latency_of_type(ConstantMultiplication.type_name(), 2) + schedule = Schedule(sfg_simple_filter, ASAPScheduler()) + + assert schedule._y_locations == {"in0": 0, "cmul0": 1, "add0": 3, "out0": 2} + schedule.reset_y_locations() + assert schedule._y_locations["in0"] is None + assert schedule._y_locations["cmul0"] is None + assert schedule._y_locations["add0"] is None + assert schedule._y_locations["add0"] is None + assert schedule._y_locations["out0"] is None + assert schedule._y_locations["foo"] is None