From e7ea2ce8ea4d41e3a3d19a87f504619807c2fc58 Mon Sep 17 00:00:00 2001
From: Simon Bjurek <simbj106@student.liu.se>
Date: Fri, 28 Feb 2025 21:19:46 +0100
Subject: [PATCH] Output times now pushed correctly, updated logger and updated
 Scheduler interface.

---
 b_asic/list_schedulers.py           |  78 +++++-
 b_asic/logger.py                    |  44 ++--
 b_asic/schedule.py                  |  21 +-
 b_asic/scheduler.py                 | 157 +++++++++---
 b_asic/scheduler_gui/compile.py     |   2 +-
 b_asic/scheduler_gui/main_window.py |   2 +-
 test/unit/test_list_schedulers.py   | 369 +++++++++++++++++++++++-----
 7 files changed, 532 insertions(+), 141 deletions(-)

diff --git a/b_asic/list_schedulers.py b/b_asic/list_schedulers.py
index 3f53a3d7..dd9550ea 100644
--- a/b_asic/list_schedulers.py
+++ b/b_asic/list_schedulers.py
@@ -1,33 +1,87 @@
 from b_asic.scheduler import ListScheduler
+from b_asic.types import GraphID, TypeName
 
 
 class EarliestDeadlineScheduler(ListScheduler):
     """Scheduler that implements the earliest-deadline-first algorithm."""
 
-    @property
-    def sort_indices(self) -> tuple[tuple[int, bool]]:
-        return ((1, True),)
+    def __init__(
+        self,
+        max_resources: dict[TypeName, int] | None = None,
+        max_concurrent_reads: int | None = None,
+        max_concurrent_writes: int | None = None,
+        input_times: dict["GraphID", int] | None = None,
+        output_delta_times: dict["GraphID", int] | None = None,
+        cyclic: bool | None = False,
+    ) -> None:
+        super().__init__(
+            max_resources=max_resources,
+            max_concurrent_reads=max_concurrent_reads,
+            max_concurrent_writes=max_concurrent_writes,
+            input_times=input_times,
+            output_delta_times=output_delta_times,
+            sort_order=((1, True),),
+        )
 
 
 class LeastSlackTimeScheduler(ListScheduler):
     """Scheduler that implements the least slack time first algorithm."""
 
-    @property
-    def sort_indices(self) -> tuple[tuple[int, bool]]:
-        return ((2, True),)
+    def __init__(
+        self,
+        max_resources: dict[TypeName, int] = None,
+        max_concurrent_reads: int = None,
+        max_concurrent_writes: int = None,
+        input_times: dict["GraphID", int] = None,
+        output_delta_times: dict["GraphID", int] = None,
+    ) -> None:
+        super().__init__(
+            max_resources=max_resources,
+            max_concurrent_reads=max_concurrent_reads,
+            max_concurrent_writes=max_concurrent_writes,
+            input_times=input_times,
+            output_delta_times=output_delta_times,
+            sort_order=((2, True),),
+        )
 
 
 class MaxFanOutScheduler(ListScheduler):
     """Scheduler that implements the maximum fan-out algorithm."""
 
-    @property
-    def sort_indices(self) -> tuple[tuple[int, bool]]:
-        return ((3, False),)
+    def __init__(
+        self,
+        max_resources: dict[TypeName, int] = None,
+        max_concurrent_reads: int = None,
+        max_concurrent_writes: int = None,
+        input_times: dict["GraphID", int] = None,
+        output_delta_times: dict["GraphID", int] = None,
+    ) -> None:
+        super().__init__(
+            max_resources=max_resources,
+            max_concurrent_reads=max_concurrent_reads,
+            max_concurrent_writes=max_concurrent_writes,
+            input_times=input_times,
+            output_delta_times=output_delta_times,
+            sort_order=((3, False),),
+        )
 
 
 class HybridScheduler(ListScheduler):
     """Scheduler that implements a hybrid algorithm. Will receive a new name once finalized."""
 
-    @property
-    def sort_indices(self) -> tuple[tuple[int, bool]]:
-        return ((2, True), (3, False))
+    def __init__(
+        self,
+        max_resources: dict[TypeName, int] = None,
+        max_concurrent_reads: int = None,
+        max_concurrent_writes: int = None,
+        input_times: dict["GraphID", int] = None,
+        output_delta_times: dict["GraphID", int] = None,
+    ) -> None:
+        super().__init__(
+            max_resources=max_resources,
+            max_concurrent_reads=max_concurrent_reads,
+            max_concurrent_writes=max_concurrent_writes,
+            input_times=input_times,
+            output_delta_times=output_delta_times,
+            sort_order=((2, True), (3, False)),
+        )
diff --git a/b_asic/logger.py b/b_asic/logger.py
index 53b510e3..2a948ce3 100644
--- a/b_asic/logger.py
+++ b/b_asic/logger.py
@@ -54,38 +54,34 @@ from logging import Logger
 from types import TracebackType
 
 
-def getLogger(source: str, filename: str, loglevel: str = "INFO") -> Logger:
+def getLogger(
+    name: str, filename: str | None = "", console_log_level: str = "warning"
+) -> Logger:
     """
     This function creates console- and filehandler and from those, creates a logger
     object.
 
     Parameters
     ----------
-    source : str
-        Source filename.
-    filename : str
-        Output filename.
-    loglevel : str, optional
-        The minimum level that the logger will log. Defaults to 'INFO'.
+    name : str
+        Name of the logger, creates a new if needed.
+    filename : str, optional
+        Name of output logfile. Defaults to "".
+    console_log_level : str, optional
+        The minimum level that the logger will log. Defaults to 'info'.
 
     Returns
     -------
     Logger : 'logging.Logger' object.
     """
 
-    # logger = logging.getLogger(name)
-    # logger = logging.getLogger('root')
-    logger = logging.getLogger(source)
+    logger = logging.getLogger(name)
 
-    # if logger 'name' already exists, return it to avoid logging duplicate
-    # messages by attaching multiple handlers of the same type
     if logger.handlers:
         return logger
-    # if logger 'name' does not already exist, create it and attach handlers
     else:
-        # set logLevel to loglevel or to INFO if requested level is incorrect
-        loglevel = getattr(logging, loglevel.upper(), logging.INFO)
-        logger.setLevel(loglevel)
+        console_log_level = getattr(logging, console_log_level.upper())
+        logger.setLevel(console_log_level)
 
         # set up the console logger
         c_fmt_date = "%T"
@@ -96,7 +92,7 @@ def getLogger(source: str, filename: str, loglevel: str = "INFO") -> Logger:
         c_formatter = logging.Formatter(c_fmt, c_fmt_date)
         c_handler = logging.StreamHandler()
         c_handler.setFormatter(c_formatter)
-        c_handler.setLevel(logging.WARNING)
+        c_handler.setLevel(console_log_level)
         logger.addHandler(c_handler)
 
         # setup the file logger
@@ -105,11 +101,13 @@ def getLogger(source: str, filename: str, loglevel: str = "INFO") -> Logger:
             "%(asctime)s %(filename)18s:%(lineno)-4s %(funcName)20s()"
             " %(levelname)-8s: %(message)s"
         )
-        f_formatter = logging.Formatter(f_fmt, f_fmt_date)
-        f_handler = logging.FileHandler(filename, mode="w")
-        f_handler.setFormatter(f_formatter)
-        f_handler.setLevel(logging.DEBUG)
-        logger.addHandler(f_handler)
+
+        if filename:
+            f_formatter = logging.Formatter(f_fmt, f_fmt_date)
+            f_handler = logging.FileHandler(filename, mode="w")
+            f_handler.setFormatter(f_formatter)
+            f_handler.setLevel(logging.DEBUG)
+            logger.addHandler(f_handler)
 
     if logger.name == "scheduler-gui.log":
         logger.info(
@@ -121,13 +119,11 @@ def getLogger(source: str, filename: str, loglevel: str = "INFO") -> Logger:
     return logger
 
 
-# log uncaught exceptions
 def handle_exceptions(
     exc_type: type[BaseException],
     exc_value: BaseException,
     exc_traceback: TracebackType | None,
 ) -> None:
-    # def log_exceptions(type, value, tb):
     """This function is a helper function to log uncaught exceptions. Install with:
     `sys.excepthook = <module>.handle_exceptions`"""
     if issubclass(exc_type, KeyboardInterrupt):
diff --git a/b_asic/schedule.py b/b_asic/schedule.py
index 13433612..2c3910de 100644
--- a/b_asic/schedule.py
+++ b/b_asic/schedule.py
@@ -352,13 +352,20 @@ class Schedule:
             if source.operation.graph_id.startswith("dontcare"):
                 available_time = 0
             else:
-                available_time = (
-                    cast(int, source.latency_offset)
-                    + self._start_times[source.operation.graph_id]
-                    - self._schedule_time * self._laps[signal.graph_id]
-                )
-                if available_time > self._schedule_time:
-                    available_time -= self._schedule_time
+                if self._schedule_time is not None:
+                    available_time = (
+                        cast(int, source.latency_offset)
+                        + self._start_times[source.operation.graph_id]
+                        - self._schedule_time * self._laps[signal.graph_id]
+                    )
+                    if available_time > self._schedule_time:
+                        available_time -= self._schedule_time
+                else:
+                    available_time = (
+                        cast(int, source.latency_offset)
+                        + self._start_times[source.operation.graph_id]
+                    )
+
             input_slacks[signal] = usage_time - available_time
         return input_slacks
 
diff --git a/b_asic/scheduler.py b/b_asic/scheduler.py
index 9a40ec02..0e9fa628 100644
--- a/b_asic/scheduler.py
+++ b/b_asic/scheduler.py
@@ -160,8 +160,27 @@ class ALAPScheduler(Scheduler):
 
 
 class ListScheduler(Scheduler, ABC):
-
-    TIME_OUT_COUNTER_LIMIT = 100
+    """
+    List-based scheduler that schedules the operations while complying to the given
+    constraints.
+
+    Parameters
+    ----------
+    max_resources : dict[TypeName, int] | None, optional
+        Max resources available to realize the schedule, by default None
+    max_concurrent_reads : int | None, optional
+        Max number of conccurent reads, by default None
+    max_concurrent_writes : int | None, optional
+        Max number of conccurent writes, by default None
+    input_times : dict[GraphID, int] | None, optional
+        Specified input times, by default None
+    output_delta_times : dict[GraphID, int] | None, optional
+        Specified output delta times, by default None
+    cyclic : bool | None, optional
+        If the scheduler is allowed to schedule cyclically (modulo), by default False
+    sort_order : tuple[tuple[int, bool]]
+        Specifies which columns in the priority table to sort on and in which order, where True is ascending order.
+    """
 
     def __init__(
         self,
@@ -170,19 +189,19 @@ class ListScheduler(Scheduler, ABC):
         max_concurrent_writes: int | None = None,
         input_times: dict["GraphID", int] | None = None,
         output_delta_times: dict["GraphID", int] | None = None,
-        cyclic: bool | None = False,
+        sort_order=tuple[tuple[int, bool], ...],
     ) -> None:
         super()
-        self._logger = logger.getLogger(__name__, "list_scheduler.log", "DEBUG")
+        self._logger = logger.getLogger("list_scheduler")
 
         if max_resources is not None:
             if not isinstance(max_resources, dict):
-                raise ValueError("max_resources must be a dictionary.")
+                raise ValueError("Provided max_resources must be a dictionary.")
             for key, value in max_resources.items():
                 if not isinstance(key, str):
-                    raise ValueError("max_resources key must be a valid type_name.")
+                    raise ValueError("Provided max_resources keys must be strings.")
                 if not isinstance(value, int):
-                    raise ValueError("max_resources value must be an integer.")
+                    raise ValueError("Provided max_resources values must be integers.")
             self._max_resources = max_resources
         else:
             self._max_resources = {}
@@ -192,16 +211,57 @@ class ListScheduler(Scheduler, ABC):
         if Output.type_name() not in self._max_resources:
             self._max_resources[Output.type_name()] = 1
 
+        if max_concurrent_reads is not None:
+            if not isinstance(max_concurrent_reads, int):
+                raise ValueError("Provided max_concurrent_reads must be an integer.")
+            if max_concurrent_reads <= 0:
+                raise ValueError("Provided max_concurrent_reads must be larger than 0.")
         self._max_concurrent_reads = max_concurrent_reads or sys.maxsize
+
+        if max_concurrent_writes is not None:
+            if not isinstance(max_concurrent_writes, int):
+                raise ValueError("Provided max_concurrent_writes must be an integer.")
+            if max_concurrent_writes <= 0:
+                raise ValueError(
+                    "Provided max_concurrent_writes must be larger than 0."
+                )
         self._max_concurrent_writes = max_concurrent_writes or sys.maxsize
 
-        self._input_times = input_times or {}
-        self._output_delta_times = output_delta_times or {}
+        if input_times is not None:
+            if not isinstance(input_times, dict):
+                raise ValueError("Provided input_times must be a dictionary.")
+            for key, value in input_times.items():
+                if not isinstance(key, str):
+                    raise ValueError("Provided input_times keys must be strings.")
+                if not isinstance(value, int):
+                    raise ValueError("Provided input_times values must be integers.")
+            if any(time < 0 for time in input_times.values()):
+                raise ValueError("Provided input_times values must be non-negative.")
+            self._input_times = input_times
+        else:
+            self._input_times = {}
 
-    @property
-    @abstractmethod
-    def sort_indices(self) -> tuple[tuple[int, bool]]:
-        raise NotImplementedError
+        if output_delta_times is not None:
+            if not isinstance(output_delta_times, dict):
+                raise ValueError("Provided output_delta_times must be a dictionary.")
+            for key, value in output_delta_times.items():
+                if not isinstance(key, str):
+                    raise ValueError(
+                        "Provided output_delta_times keys must be strings."
+                    )
+                if not isinstance(value, int):
+                    raise ValueError(
+                        "Provided output_delta_times values must be integers."
+                    )
+            if any(time < 0 for time in output_delta_times.values()):
+                raise ValueError(
+                    "Provided output_delta_times values must be non-negative."
+                )
+            self._output_delta_times = output_delta_times
+        else:
+            self._output_delta_times = {}
+
+        self._sort_order = sort_order
 
     def apply_scheduling(self, schedule: "Schedule") -> None:
         """Applies the scheduling algorithm on the given Schedule.
@@ -216,7 +276,25 @@ class ListScheduler(Scheduler, ABC):
         self._schedule = schedule
         self._sfg = schedule.sfg
 
-        if self._schedule.cyclic and self._schedule.schedule_time is None:
+        for resource_type in self._max_resources.keys():
+            if not self._sfg.find_by_type_name(resource_type):
+                raise ValueError(
+                    f"Provided max resource of type {resource_type} cannot be found in the provided SFG."
+                )
+
+        for key in self._input_times.keys():
+            if self._sfg.find_by_id(key) is None:
+                raise ValueError(
+                    f"Provided input time with GraphID {key} cannot be found in the provided SFG."
+                )
+
+        for key in self._output_delta_times.keys():
+            if self._sfg.find_by_id(key) is None:
+                raise ValueError(
+                    f"Provided output delta time with GraphID {key} cannot be found in the provided SFG."
+                )
+
+        if self._schedule._cyclic and self._schedule.schedule_time is None:
             raise ValueError("Scheduling time must be provided when cyclic = True.")
 
         for resource_type, resource_amount in self._max_resources.items():
@@ -239,7 +317,7 @@ class ListScheduler(Scheduler, ABC):
         alap_start_times = alap_schedule.start_times
         self._schedule.start_times = {}
 
-        if not self._schedule.cyclic and self._schedule.schedule_time:
+        if not self._schedule._cyclic and self._schedule.schedule_time:
             if alap_schedule.schedule_time > self._schedule.schedule_time:
                 raise ValueError(
                     f"Provided scheduling time {schedule.schedule_time} cannot be reached, "
@@ -268,7 +346,6 @@ class ListScheduler(Scheduler, ABC):
         self.remaining_reads = self._max_concurrent_reads
 
         self._current_time = 0
-        self._time_out_counter = 0
         self._op_laps = {}
 
         self._remaining_ops = [
@@ -310,7 +387,6 @@ class ListScheduler(Scheduler, ABC):
                     op_id for op_id in self._remaining_ops if op_id != next_op.graph_id
                 ]
 
-                self._time_out_counter = 0
                 self._schedule.place_operation(next_op, self._current_time)
                 self._op_laps[next_op.graph_id] = (
                     (self._current_time) // self._schedule.schedule_time
@@ -329,7 +405,7 @@ class ListScheduler(Scheduler, ABC):
 
                 ready_ops_priority_table = self._get_ready_ops_priority_table()
 
-            self._go_to_next_time_step()
+            self._current_time += 1
             self.remaining_reads = self._max_concurrent_reads
 
         self._logger.debug("--- Operation scheduling completed ---")
@@ -353,22 +429,13 @@ class ListScheduler(Scheduler, ABC):
         self._schedule.sort_y_locations_on_start_times()
         self._logger.debug("--- Scheduling completed ---")
 
-    def _go_to_next_time_step(self):
-        self._time_out_counter += 1
-        if self._time_out_counter >= self.TIME_OUT_COUNTER_LIMIT:
-            raise TimeoutError(
-                "Algorithm did not manage to schedule any operation for 10 time steps, "
-                "try relaxing the constraints."
-            )
-        self._current_time += 1
-
     def _get_next_op_id(
         self, ready_ops_priority_table: list[tuple["GraphID", int, ...]]
     ) -> "GraphID":
         def sort_key(item):
             return tuple(
                 (item[index] * (-1 if not asc else 1),)
-                for index, asc in self.sort_indices
+                for index, asc in self._sort_order
             )
 
         sorted_table = sorted(ready_ops_priority_table, key=sort_key)
@@ -492,7 +559,7 @@ class ListScheduler(Scheduler, ABC):
 
     def _handle_outputs(self) -> None:
         self._logger.debug("--- Output placement starting ---")
-        if self._schedule.cyclic:
+        if self._schedule._cyclic:
             end = self._schedule.schedule_time
         else:
             end = self._schedule.get_max_end_time()
@@ -503,7 +570,7 @@ class ListScheduler(Scheduler, ABC):
 
                 new_time = end + delta_time
 
-                if self._schedule.cyclic and self._schedule.schedule_time is not None:
+                if self._schedule._cyclic and self._schedule.schedule_time is not None:
                     self._schedule.place_operation(output, new_time)
                 else:
                     self._schedule.start_times[output.graph_id] = new_time
@@ -531,5 +598,33 @@ class ListScheduler(Scheduler, ABC):
                     else new_time
                 )
                 self._logger.debug(f"   {output.graph_id} time: {modulo_time}")
-
         self._logger.debug("--- Output placement completed ---")
+
+        self._logger.debug("--- Output placement optimization starting ---")
+        min_slack = min(
+            self._schedule.backward_slack(op.graph_id)
+            for op in self._sfg.find_by_type_name(Output.type_name())
+        )
+        if min_slack > 0:
+            for output in self._sfg.find_by_type_name(Output.type_name()):
+                if self._schedule._cyclic and self._schedule.schedule_time is not None:
+                    self._schedule.move_operation(output.graph_id, -min_slack)
+                else:
+                    self._schedule.start_times[output.graph_id] = (
+                        self._schedule.start_times[output.graph_id] - min_slack
+                    )
+                new_time = self._schedule.start_times[output.graph_id]
+                if (
+                    not self._schedule._cyclic
+                    and self._schedule.schedule_time is not None
+                ):
+                    if new_time > self._schedule.schedule_time:
+                        raise ValueError(
+                            f"Cannot place output {output.graph_id} at time {new_time} "
+                            f"for scheduling time {self._schedule.schedule_time}. "
+                            "Try to relax the scheduling time, change the output delta times or enable cyclic."
+                        )
+                self._logger.debug(
+                    f"   {output.graph_id} moved {min_slack} time steps backwards to new time {new_time}"
+                )
+        self._logger.debug("--- Output placement optimization completed ---")
diff --git a/b_asic/scheduler_gui/compile.py b/b_asic/scheduler_gui/compile.py
index 5a7fcff4..922b6ecf 100644
--- a/b_asic/scheduler_gui/compile.py
+++ b/b_asic/scheduler_gui/compile.py
@@ -20,7 +20,7 @@ from setuptools_scm import get_version
 try:
     import b_asic.logger as logger
 
-    log = logger.getLogger(__name__, "scheduler-gui.log")
+    log = logger.getLogger(__name__)
     sys.excepthook = logger.handle_exceptions
 except ModuleNotFoundError:
     log = None
diff --git a/b_asic/scheduler_gui/main_window.py b/b_asic/scheduler_gui/main_window.py
index 09756521..9bb7dad9 100644
--- a/b_asic/scheduler_gui/main_window.py
+++ b/b_asic/scheduler_gui/main_window.py
@@ -81,7 +81,7 @@ from b_asic.scheduler_gui.ui_main_window import Ui_MainWindow
 if TYPE_CHECKING:
     from logging import Logger
 
-log: "Logger" = logger.getLogger(__name__, "scheduler-gui.log")
+log: "Logger" = logger.getLogger(__name__)
 sys.excepthook = logger.handle_exceptions
 
 
diff --git a/test/unit/test_list_schedulers.py b/test/unit/test_list_schedulers.py
index 252baa3d..fa349f87 100644
--- a/test/unit/test_list_schedulers.py
+++ b/test/unit/test_list_schedulers.py
@@ -621,14 +621,14 @@ class TestHybridScheduler:
             "in7": 7,
         }
         output_times = {
-            "out0": -2,
-            "out1": -1,
-            "out2": 0,
-            "out3": 1,
-            "out4": 2,
-            "out5": 3,
-            "out6": 4,
-            "out7": 5,
+            "out0": 0,
+            "out1": 1,
+            "out2": 2,
+            "out3": 3,
+            "out4": 4,
+            "out5": 5,
+            "out6": 6,
+            "out7": 7,
         }
         schedule = Schedule(
             sfg,
@@ -665,14 +665,14 @@ class TestHybridScheduler:
             "cmul1": 15,
             "bfly10": 16,
             "bfly4": 17,
-            "out0": 18,
-            "out1": 19,
-            "out2": 20,
-            "out3": 1,
-            "out4": 2,
-            "out5": 3,
-            "out6": 4,
-            "out7": 5,
+            "out0": 17,
+            "out1": 18,
+            "out2": 19,
+            "out3": 20,
+            "out4": 1,
+            "out5": 2,
+            "out6": 3,
+            "out7": 4,
         }
         assert schedule.schedule_time == 20
 
@@ -696,14 +696,14 @@ class TestHybridScheduler:
             "in7": 7,
         }
         output_times = {
-            "out0": -2,
-            "out1": -1,
-            "out2": 0,
-            "out3": 1,
-            "out4": 2,
-            "out5": 3,
-            "out6": 4,
-            "out7": 5,
+            "out0": 0,
+            "out1": 1,
+            "out2": 2,
+            "out3": 3,
+            "out4": 4,
+            "out5": 5,
+            "out6": 6,
+            "out7": 7,
         }
         schedule = Schedule(
             sfg,
@@ -739,16 +739,16 @@ class TestHybridScheduler:
             "cmul1": 15,
             "bfly10": 16,
             "bfly4": 17,
-            "out0": 18,
-            "out1": 19,
-            "out2": 20,
-            "out3": 21,
-            "out4": 22,
-            "out5": 23,
-            "out6": 24,
-            "out7": 25,
+            "out0": 17,
+            "out1": 18,
+            "out2": 19,
+            "out3": 20,
+            "out4": 21,
+            "out5": 22,
+            "out6": 23,
+            "out7": 24,
         }
-        assert schedule.schedule_time == 25
+        assert schedule.schedule_time == 24
 
     def test_ldlt_inverse_2x2(self):
         sfg = ldlt_matrix_inverse(N=2)
@@ -833,7 +833,7 @@ class TestHybridScheduler:
         }
         assert schedule.schedule_time == 16
 
-    def test_max_invalid_resources(self):
+    def test_invalid_max_resources(self):
         sfg = ldlt_matrix_inverse(N=2)
 
         sfg.set_latency_of_type(MADS.type_name(), 3)
@@ -842,27 +842,256 @@ class TestHybridScheduler:
         sfg.set_execution_time_of_type(Reciprocal.type_name(), 1)
 
         resources = 2
-        with pytest.raises(ValueError, match="max_resources must be a dictionary."):
+        with pytest.raises(
+            ValueError, match="Provided max_resources must be a dictionary."
+        ):
             Schedule(sfg, scheduler=HybridScheduler(resources))
 
         resources = "test"
-        with pytest.raises(ValueError, match="max_resources must be a dictionary."):
+        with pytest.raises(
+            ValueError, match="Provided max_resources must be a dictionary."
+        ):
             Schedule(sfg, scheduler=HybridScheduler(resources))
 
         resources = []
-        with pytest.raises(ValueError, match="max_resources must be a dictionary."):
+        with pytest.raises(
+            ValueError, match="Provided max_resources must be a dictionary."
+        ):
             Schedule(sfg, scheduler=HybridScheduler(resources))
 
         resources = {1: 1}
         with pytest.raises(
-            ValueError, match="max_resources key must be a valid type_name."
+            ValueError, match="Provided max_resources keys must be strings."
         ):
             Schedule(sfg, scheduler=HybridScheduler(resources))
 
         resources = {MADS.type_name(): "test"}
-        with pytest.raises(ValueError, match="max_resources value must be an integer."):
+        with pytest.raises(
+            ValueError, match="Provided max_resources values must be integers."
+        ):
             Schedule(sfg, scheduler=HybridScheduler(resources))
 
+    def test_invalid_max_concurrent_writes(self):
+        sfg = ldlt_matrix_inverse(N=2)
+
+        sfg.set_latency_of_type(MADS.type_name(), 3)
+        sfg.set_latency_of_type(Reciprocal.type_name(), 2)
+        sfg.set_execution_time_of_type(MADS.type_name(), 1)
+        sfg.set_execution_time_of_type(Reciprocal.type_name(), 1)
+
+        max_concurrent_writes = "5"
+        with pytest.raises(
+            ValueError, match="Provided max_concurrent_writes must be an integer."
+        ):
+            Schedule(
+                sfg,
+                scheduler=HybridScheduler(max_concurrent_writes=max_concurrent_writes),
+            )
+
+        max_concurrent_writes = 0
+        with pytest.raises(
+            ValueError, match="Provided max_concurrent_writes must be larger than 0."
+        ):
+            Schedule(
+                sfg,
+                scheduler=HybridScheduler(max_concurrent_writes=max_concurrent_writes),
+            )
+
+        max_concurrent_writes = -1
+        with pytest.raises(
+            ValueError, match="Provided max_concurrent_writes must be larger than 0."
+        ):
+            Schedule(
+                sfg,
+                scheduler=HybridScheduler(max_concurrent_writes=max_concurrent_writes),
+            )
+
+    def test_invalid_max_concurrent_reads(self):
+        sfg = ldlt_matrix_inverse(N=2)
+
+        sfg.set_latency_of_type(MADS.type_name(), 3)
+        sfg.set_latency_of_type(Reciprocal.type_name(), 2)
+        sfg.set_execution_time_of_type(MADS.type_name(), 1)
+        sfg.set_execution_time_of_type(Reciprocal.type_name(), 1)
+
+        max_concurrent_reads = "5"
+        with pytest.raises(
+            ValueError, match="Provided max_concurrent_reads must be an integer."
+        ):
+            Schedule(
+                sfg,
+                scheduler=HybridScheduler(max_concurrent_reads=max_concurrent_reads),
+            )
+
+        max_concurrent_reads = 0
+        with pytest.raises(
+            ValueError, match="Provided max_concurrent_reads must be larger than 0."
+        ):
+            Schedule(
+                sfg,
+                scheduler=HybridScheduler(max_concurrent_reads=max_concurrent_reads),
+            )
+
+        max_concurrent_reads = -1
+        with pytest.raises(
+            ValueError, match="Provided max_concurrent_reads must be larger than 0."
+        ):
+            Schedule(
+                sfg,
+                scheduler=HybridScheduler(max_concurrent_reads=max_concurrent_reads),
+            )
+
+    def test_invalid_input_times(self):
+        sfg = ldlt_matrix_inverse(N=2)
+
+        sfg.set_latency_of_type(MADS.type_name(), 3)
+        sfg.set_latency_of_type(Reciprocal.type_name(), 2)
+        sfg.set_execution_time_of_type(MADS.type_name(), 1)
+        sfg.set_execution_time_of_type(Reciprocal.type_name(), 1)
+
+        input_times = 5
+        with pytest.raises(
+            ValueError, match="Provided input_times must be a dictionary."
+        ):
+            Schedule(sfg, scheduler=HybridScheduler(input_times=input_times))
+
+        input_times = "test1"
+        with pytest.raises(
+            ValueError, match="Provided input_times must be a dictionary."
+        ):
+            Schedule(sfg, scheduler=HybridScheduler(input_times=input_times))
+
+        input_times = []
+        with pytest.raises(
+            ValueError, match="Provided input_times must be a dictionary."
+        ):
+            Schedule(sfg, scheduler=HybridScheduler(input_times=input_times))
+
+        input_times = {3: 3}
+        with pytest.raises(
+            ValueError, match="Provided input_times keys must be strings."
+        ):
+            Schedule(sfg, scheduler=HybridScheduler(input_times=input_times))
+
+        input_times = {"in0": "foo"}
+        with pytest.raises(
+            ValueError, match="Provided input_times values must be integers."
+        ):
+            Schedule(sfg, scheduler=HybridScheduler(input_times=input_times))
+
+        input_times = {"in0": -1}
+        with pytest.raises(
+            ValueError, match="Provided input_times values must be non-negative."
+        ):
+            Schedule(sfg, scheduler=HybridScheduler(input_times=input_times))
+
+    def test_invalid_output_delta_times(self):
+        sfg = ldlt_matrix_inverse(N=2)
+
+        sfg.set_latency_of_type(MADS.type_name(), 3)
+        sfg.set_latency_of_type(Reciprocal.type_name(), 2)
+        sfg.set_execution_time_of_type(MADS.type_name(), 1)
+        sfg.set_execution_time_of_type(Reciprocal.type_name(), 1)
+
+        output_delta_times = 10
+        with pytest.raises(
+            ValueError, match="Provided output_delta_times must be a dictionary."
+        ):
+            Schedule(
+                sfg, scheduler=HybridScheduler(output_delta_times=output_delta_times)
+            )
+
+        output_delta_times = "test2"
+        with pytest.raises(
+            ValueError, match="Provided output_delta_times must be a dictionary."
+        ):
+            Schedule(
+                sfg, scheduler=HybridScheduler(output_delta_times=output_delta_times)
+            )
+
+        output_delta_times = []
+        with pytest.raises(
+            ValueError, match="Provided output_delta_times must be a dictionary."
+        ):
+            Schedule(
+                sfg, scheduler=HybridScheduler(output_delta_times=output_delta_times)
+            )
+
+        output_delta_times = {4: 4}
+        with pytest.raises(
+            ValueError, match="Provided output_delta_times keys must be strings."
+        ):
+            Schedule(
+                sfg, scheduler=HybridScheduler(output_delta_times=output_delta_times)
+            )
+
+        output_delta_times = {"out0": "foo"}
+        with pytest.raises(
+            ValueError, match="Provided output_delta_times values must be integers."
+        ):
+            Schedule(
+                sfg, scheduler=HybridScheduler(output_delta_times=output_delta_times)
+            )
+
+        output_delta_times = {"out0": -1}
+        with pytest.raises(
+            ValueError, match="Provided output_delta_times values must be non-negative."
+        ):
+            Schedule(
+                sfg, scheduler=HybridScheduler(output_delta_times=output_delta_times)
+            )
+
+    def test_resource_not_in_sfg(self):
+        sfg = ldlt_matrix_inverse(N=3)
+
+        sfg.set_latency_of_type(MADS.type_name(), 3)
+        sfg.set_latency_of_type(Reciprocal.type_name(), 2)
+        sfg.set_execution_time_of_type(MADS.type_name(), 1)
+        sfg.set_execution_time_of_type(Reciprocal.type_name(), 1)
+
+        resources = {
+            MADS.type_name(): 1,
+            Reciprocal.type_name(): 1,
+            Addition.type_name(): 2,
+        }
+        with pytest.raises(
+            ValueError,
+            match="Provided max resource of type add cannot be found in the provided SFG.",
+        ):
+            Schedule(sfg, scheduler=HybridScheduler(resources))
+
+    def test_input_not_in_sfg(self):
+        sfg = ldlt_matrix_inverse(N=2)
+
+        sfg.set_latency_of_type(MADS.type_name(), 3)
+        sfg.set_latency_of_type(Reciprocal.type_name(), 2)
+        sfg.set_execution_time_of_type(MADS.type_name(), 1)
+        sfg.set_execution_time_of_type(Reciprocal.type_name(), 1)
+
+        input_times = {"in100": 4}
+        with pytest.raises(
+            ValueError,
+            match="Provided input time with GraphID in100 cannot be found in the provided SFG.",
+        ):
+            Schedule(sfg, scheduler=HybridScheduler(input_times=input_times))
+
+    def test_output_not_in_sfg(self):
+        sfg = ldlt_matrix_inverse(N=2)
+
+        sfg.set_latency_of_type(MADS.type_name(), 3)
+        sfg.set_latency_of_type(Reciprocal.type_name(), 2)
+        sfg.set_execution_time_of_type(MADS.type_name(), 1)
+        sfg.set_execution_time_of_type(Reciprocal.type_name(), 1)
+
+        output_delta_times = {"out90": 2}
+        with pytest.raises(
+            ValueError,
+            match="Provided output delta time with GraphID out90 cannot be found in the provided SFG.",
+        ):
+            Schedule(
+                sfg, scheduler=HybridScheduler(output_delta_times=output_delta_times)
+            )
+
     def test_ldlt_inverse_3x3_read_and_write_constrained(self):
         sfg = ldlt_matrix_inverse(N=3)
 
@@ -886,27 +1115,6 @@ class TestHybridScheduler:
         assert mem_vars.read_ports_bound() == 3
         assert mem_vars.write_ports_bound() == 1
 
-    def test_read_constrained_too_tight(self):
-        sfg = ldlt_matrix_inverse(N=2)
-
-        sfg.set_latency_of_type(MADS.type_name(), 3)
-        sfg.set_latency_of_type(Reciprocal.type_name(), 2)
-        sfg.set_execution_time_of_type(MADS.type_name(), 1)
-        sfg.set_execution_time_of_type(Reciprocal.type_name(), 1)
-
-        resources = {MADS.type_name(): 1, Reciprocal.type_name(): 1}
-        with pytest.raises(
-            TimeoutError,
-            match="Algorithm did not manage to schedule any operation for 10 time steps, try relaxing the constraints.",
-        ):
-            Schedule(
-                sfg,
-                scheduler=HybridScheduler(
-                    max_resources=resources,
-                    max_concurrent_reads=2,
-                ),
-            )
-
     def test_32_point_fft_custom_io_times(self):
         POINTS = 32
         sfg = radix_2_dif_fft(POINTS)
@@ -930,10 +1138,7 @@ class TestHybridScheduler:
 
         for i in range(POINTS):
             assert schedule.start_times[f"in{i}"] == i
-            assert (
-                schedule.start_times[f"out{i}"]
-                == schedule.get_max_non_io_end_time() + i
-            )
+            assert schedule.start_times[f"out{i}"] == 95 + i
 
     # Too slow for pipeline right now
     # def test_64_point_fft_custom_io_times(self):
@@ -989,7 +1194,13 @@ class TestHybridScheduler:
 
         for i in range(POINTS):
             assert schedule.start_times[f"in{i}"] == i
-            assert schedule.start_times[f"out{i}"] == 96 if i == 0 else i
+            if i == 0:
+                expected_value = 95
+            elif i == 1:
+                expected_value = 96
+            else:
+                expected_value = i - 1
+            assert schedule.start_times[f"out{i}"] == expected_value
 
     def test_cyclic_scheduling(self):
         sfg = radix_2_dif_fft(points=4)
@@ -1311,3 +1522,31 @@ class TestHybridScheduler:
             's3': 0,
         }
         assert schedule.schedule_time == 4
+
+    def test_invalid_output_delta_time(self):
+        sfg = radix_2_dif_fft(points=4)
+
+        sfg.set_latency_of_type(Butterfly.type_name(), 1)
+        sfg.set_latency_of_type(ConstantMultiplication.type_name(), 3)
+        sfg.set_execution_time_of_type(Butterfly.type_name(), 1)
+        sfg.set_execution_time_of_type(ConstantMultiplication.type_name(), 1)
+
+        resources = {
+            Butterfly.type_name(): 1,
+            ConstantMultiplication.type_name(): 1,
+            Input.type_name(): 2,
+            Output.type_name(): 2,
+        }
+        output_delta_times = {"out0": 0, "out1": 1, "out2": 2, "out3": 3}
+
+        with pytest.raises(
+            ValueError,
+            match="Cannot place output out2 at time 6 for scheduling time 5. Try to relax the scheduling time, change the output delta times or enable cyclic.",
+        ):
+            Schedule(
+                sfg,
+                scheduler=HybridScheduler(
+                    resources, output_delta_times=output_delta_times
+                ),
+                schedule_time=5,
+            )
-- 
GitLab