From f42c18bb9ac3c2cdd1393b501fb9417f930b18d1 Mon Sep 17 00:00:00 2001
From: Simon Bjurek <simbj106@student.liu.se>
Date: Fri, 21 Feb 2025 18:08:44 +0100
Subject: [PATCH] List scheduler now refactored to sort continously given
 indecies by subclass.

---
 b_asic/__init__.py                            |   2 +-
 b_asic/core_schedulers.py                     | 206 ----------
 b_asic/list_schedulers.py                     |  33 ++
 b_asic/scheduler.py                           | 339 ++++++++++++----
 b_asic/signal_flow_graph.py                   |   2 +-
 .../auto_scheduling_with_custom_io_times.py   |   3 +-
 examples/fivepointwinograddft.py              |   2 +-
 examples/folding_example_with_architecture.py |   2 +-
 examples/ldlt_matrix_inverse.py               |   5 +-
 examples/lwdfallpass.py                       |   2 +-
 examples/memory_constrained_scheduling.py     |   3 +-
 examples/secondorderdirectformiir.py          |   2 +-
 .../secondorderdirectformiir_architecture.py  |   2 +-
 examples/thirdorderblwdf.py                   |   2 +-
 examples/threepointwinograddft.py             |   2 +-
 test/fixtures/schedule.py                     |   2 +-
 test/integration/test_sfg_to_architecture.py  |   3 +-
 test/unit/test_architecture.py                |   2 +-
 ..._schedulers.py => test_list_schedulers.py} | 371 ++++--------------
 test/unit/test_schedule.py                    |   2 +-
 test/unit/test_scheduler.py                   | 266 +++++++++++++
 test/unit/test_scheduler_gui.py               |   2 +-
 22 files changed, 673 insertions(+), 582 deletions(-)
 delete mode 100644 b_asic/core_schedulers.py
 create mode 100644 b_asic/list_schedulers.py
 rename test/unit/{test_core_schedulers.py => test_list_schedulers.py} (75%)
 create mode 100644 test/unit/test_scheduler.py

diff --git a/b_asic/__init__.py b/b_asic/__init__.py
index 5b365ed1..1d0af5f6 100644
--- a/b_asic/__init__.py
+++ b/b_asic/__init__.py
@@ -4,8 +4,8 @@ ASIC toolbox that simplifies circuit design and optimization.
 
 # Python modules.
 from b_asic.core_operations import *
-from b_asic.core_schedulers import *
 from b_asic.graph_component import *
+from b_asic.list_schedulers import *
 from b_asic.operation import *
 from b_asic.port import *
 from b_asic.save_load_structure import *
diff --git a/b_asic/core_schedulers.py b/b_asic/core_schedulers.py
deleted file mode 100644
index cce720e3..00000000
--- a/b_asic/core_schedulers.py
+++ /dev/null
@@ -1,206 +0,0 @@
-import copy
-from typing import TYPE_CHECKING, cast
-
-from b_asic.scheduler import ListScheduler, Scheduler
-from b_asic.special_operations import Delay, Output
-
-if TYPE_CHECKING:
-    from b_asic.schedule import Schedule
-    from b_asic.types import GraphID
-
-
-class ASAPScheduler(Scheduler):
-    """Scheduler that implements the as-soon-as-possible (ASAP) algorithm."""
-
-    def apply_scheduling(self, schedule: "Schedule") -> None:
-        """Applies the scheduling algorithm on the given Schedule.
-
-        Parameters
-        ----------
-        schedule : Schedule
-            Schedule to apply the scheduling algorithm on.
-        """
-        prec_list = schedule.sfg.get_precedence_list()
-        if len(prec_list) < 2:
-            raise ValueError("Empty signal flow graph cannot be scheduled.")
-
-        # handle the first set in precedence graph (input and delays)
-        non_schedulable_ops = []
-        for outport in prec_list[0]:
-            operation = outport.operation
-            if operation.type_name() == Delay.type_name():
-                non_schedulable_ops.append(operation.graph_id)
-            else:
-                schedule.start_times[operation.graph_id] = 0
-
-        # handle second set in precedence graph (first operations)
-        for outport in prec_list[1]:
-            operation = outport.operation
-            schedule.start_times[operation.graph_id] = 0
-
-        # handle the remaining sets
-        for outports in prec_list[2:]:
-            for outport in outports:
-                operation = outport.operation
-                if operation.graph_id not in schedule.start_times:
-                    op_start_time = 0
-                    for current_input in operation.inputs:
-                        source_port = current_input.signals[0].source
-
-                        if source_port.operation.graph_id in non_schedulable_ops:
-                            source_end_time = 0
-                        else:
-                            source_op_time = schedule.start_times[
-                                source_port.operation.graph_id
-                            ]
-
-                            if source_port.latency_offset is None:
-                                raise ValueError(
-                                    f"Output port {source_port.index} of"
-                                    " operation"
-                                    f" {source_port.operation.graph_id} has no"
-                                    " latency-offset."
-                                )
-
-                            source_end_time = (
-                                source_op_time + source_port.latency_offset
-                            )
-
-                        if current_input.latency_offset is None:
-                            raise ValueError(
-                                f"Input port {current_input.index} of operation"
-                                f" {current_input.operation.graph_id} has no"
-                                " latency-offset."
-                            )
-                        op_start_time_from_in = (
-                            source_end_time - current_input.latency_offset
-                        )
-                        op_start_time = max(op_start_time, op_start_time_from_in)
-
-                    schedule.start_times[operation.graph_id] = op_start_time
-
-        self._handle_outputs(schedule, non_schedulable_ops)
-        schedule.remove_delays()
-
-
-class ALAPScheduler(Scheduler):
-    """Scheduler that implements the as-late-as-possible (ALAP) algorithm."""
-
-    def apply_scheduling(self, schedule: "Schedule") -> None:
-        """Applies the scheduling algorithm on the given Schedule.
-
-        Parameters
-        ----------
-        schedule : Schedule
-            Schedule to apply the scheduling algorithm on.
-        """
-        ASAPScheduler().apply_scheduling(schedule)
-        max_end_time = schedule.get_max_end_time()
-
-        if schedule.schedule_time is None:
-            schedule.set_schedule_time(max_end_time)
-        elif schedule.schedule_time < max_end_time:
-            raise ValueError(f"Too short schedule time. Minimum is {max_end_time}.")
-
-        # move all outputs ALAP before operations
-        for output in schedule.sfg.find_by_type_name(Output.type_name()):
-            output = cast(Output, output)
-            schedule.move_operation_alap(output.graph_id)
-
-        # move all operations ALAP
-        for step in reversed(schedule.sfg.get_precedence_list()):
-            for outport in step:
-                if not isinstance(outport.operation, Delay):
-                    schedule.move_operation_alap(outport.operation.graph_id)
-
-
-class EarliestDeadlineScheduler(ListScheduler):
-    """Scheduler that implements the earliest-deadline-first algorithm."""
-
-    @staticmethod
-    def _get_sorted_operations(schedule: "Schedule") -> list["GraphID"]:
-        schedule_copy = copy.copy(schedule)
-        ALAPScheduler().apply_scheduling(schedule_copy)
-
-        deadlines = {}
-        for op_id, start_time in schedule_copy.start_times.items():
-            deadlines[op_id] = start_time + schedule.sfg.find_by_id(op_id).latency
-
-        return sorted(deadlines, key=deadlines.get)
-
-
-class LeastSlackTimeScheduler(ListScheduler):
-    """Scheduler that implements the least slack time first algorithm."""
-
-    @staticmethod
-    def _get_sorted_operations(schedule: "Schedule") -> list["GraphID"]:
-        schedule_copy = copy.copy(schedule)
-        ALAPScheduler().apply_scheduling(schedule_copy)
-
-        sorted_ops = sorted(
-            schedule_copy.start_times, key=schedule_copy.start_times.get
-        )
-        return sorted_ops
-
-
-class MaxFanOutScheduler(ListScheduler):
-    """Scheduler that implements the maximum fan-out algorithm."""
-
-    @staticmethod
-    def _get_sorted_operations(schedule: "Schedule") -> list["GraphID"]:
-        schedule_copy = copy.copy(schedule)
-        ALAPScheduler().apply_scheduling(schedule_copy)
-
-        fan_outs = {}
-        for op_id, start_time in schedule_copy.start_times.items():
-            fan_outs[op_id] = len(schedule.sfg.find_by_id(op_id).output_signals)
-
-        sorted_ops = sorted(fan_outs, key=fan_outs.get, reverse=True)
-        return sorted_ops
-
-
-class HybridScheduler(ListScheduler):
-    """Scheduler that implements a hybrid algorithm. Will receive a new name once finalized."""
-
-    @staticmethod
-    def _get_sorted_operations(schedule: "Schedule") -> list["GraphID"]:
-        # sort least-slack and then resort ties according to max-fan-out
-        schedule_copy = copy.copy(schedule)
-        ALAPScheduler().apply_scheduling(schedule_copy)
-
-        sorted_items = sorted(
-            schedule_copy.start_times.items(), key=lambda item: item[1]
-        )
-
-        fan_out_sorted_items = []
-
-        last_value = sorted_items[0][0]
-        current_group = []
-        for key, value in sorted_items:
-
-            if value != last_value:
-                # the group is completed, sort it internally
-                sorted_group = sorted(
-                    current_group,
-                    key=lambda pair: len(
-                        schedule.sfg.find_by_id(pair[0]).output_signals
-                    ),
-                    reverse=True,
-                )
-                fan_out_sorted_items += sorted_group
-                current_group = []
-
-            current_group.append((key, value))
-
-            last_value = value
-
-        sorted_group = sorted(
-            current_group,
-            key=lambda pair: len(schedule.sfg.find_by_id(pair[0]).output_signals),
-            reverse=True,
-        )
-        fan_out_sorted_items += sorted_group
-
-        sorted_op_list = [pair[0] for pair in fan_out_sorted_items]
-
-        return sorted_op_list
diff --git a/b_asic/list_schedulers.py b/b_asic/list_schedulers.py
new file mode 100644
index 00000000..3f53a3d7
--- /dev/null
+++ b/b_asic/list_schedulers.py
@@ -0,0 +1,33 @@
+from b_asic.scheduler import ListScheduler
+
+
+class EarliestDeadlineScheduler(ListScheduler):
+    """Scheduler that implements the earliest-deadline-first algorithm."""
+
+    @property
+    def sort_indices(self) -> tuple[tuple[int, bool]]:
+        return ((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),)
+
+
+class MaxFanOutScheduler(ListScheduler):
+    """Scheduler that implements the maximum fan-out algorithm."""
+
+    @property
+    def sort_indices(self) -> tuple[tuple[int, bool]]:
+        return ((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))
diff --git a/b_asic/scheduler.py b/b_asic/scheduler.py
index bf9a2474..fc512557 100644
--- a/b_asic/scheduler.py
+++ b/b_asic/scheduler.py
@@ -1,3 +1,4 @@
+import copy
 import sys
 from abc import ABC, abstractmethod
 from typing import TYPE_CHECKING, Optional, cast
@@ -46,6 +47,111 @@ class Scheduler(ABC):
                 ] + cast(int, source_port.latency_offset)
 
 
+class ASAPScheduler(Scheduler):
+    """Scheduler that implements the as-soon-as-possible (ASAP) algorithm."""
+
+    def apply_scheduling(self, schedule: "Schedule") -> None:
+        """Applies the scheduling algorithm on the given Schedule.
+
+        Parameters
+        ----------
+        schedule : Schedule
+            Schedule to apply the scheduling algorithm on.
+        """
+        prec_list = schedule.sfg.get_precedence_list()
+        if len(prec_list) < 2:
+            raise ValueError("Empty signal flow graph cannot be scheduled.")
+
+        # handle the first set in precedence graph (input and delays)
+        non_schedulable_ops = []
+        for outport in prec_list[0]:
+            operation = outport.operation
+            if operation.type_name() == Delay.type_name():
+                non_schedulable_ops.append(operation.graph_id)
+            else:
+                schedule.start_times[operation.graph_id] = 0
+
+        # handle second set in precedence graph (first operations)
+        for outport in prec_list[1]:
+            operation = outport.operation
+            schedule.start_times[operation.graph_id] = 0
+
+        # handle the remaining sets
+        for outports in prec_list[2:]:
+            for outport in outports:
+                operation = outport.operation
+                if operation.graph_id not in schedule.start_times:
+                    op_start_time = 0
+                    for current_input in operation.inputs:
+                        source_port = current_input.signals[0].source
+
+                        if source_port.operation.graph_id in non_schedulable_ops:
+                            source_end_time = 0
+                        else:
+                            source_op_time = schedule.start_times[
+                                source_port.operation.graph_id
+                            ]
+
+                            if source_port.latency_offset is None:
+                                raise ValueError(
+                                    f"Output port {source_port.index} of"
+                                    " operation"
+                                    f" {source_port.operation.graph_id} has no"
+                                    " latency-offset."
+                                )
+
+                            source_end_time = (
+                                source_op_time + source_port.latency_offset
+                            )
+
+                        if current_input.latency_offset is None:
+                            raise ValueError(
+                                f"Input port {current_input.index} of operation"
+                                f" {current_input.operation.graph_id} has no"
+                                " latency-offset."
+                            )
+                        op_start_time_from_in = (
+                            source_end_time - current_input.latency_offset
+                        )
+                        op_start_time = max(op_start_time, op_start_time_from_in)
+
+                    schedule.start_times[operation.graph_id] = op_start_time
+
+        self._handle_outputs(schedule, non_schedulable_ops)
+        schedule.remove_delays()
+
+
+class ALAPScheduler(Scheduler):
+    """Scheduler that implements the as-late-as-possible (ALAP) algorithm."""
+
+    def apply_scheduling(self, schedule: "Schedule") -> None:
+        """Applies the scheduling algorithm on the given Schedule.
+
+        Parameters
+        ----------
+        schedule : Schedule
+            Schedule to apply the scheduling algorithm on.
+        """
+        ASAPScheduler().apply_scheduling(schedule)
+        max_end_time = schedule.get_max_end_time()
+
+        if schedule.schedule_time is None:
+            schedule.set_schedule_time(max_end_time)
+        elif schedule.schedule_time < max_end_time:
+            raise ValueError(f"Too short schedule time. Minimum is {max_end_time}.")
+
+        # move all outputs ALAP before operations
+        for output in schedule.sfg.find_by_type_name(Output.type_name()):
+            output = cast(Output, output)
+            schedule.move_operation_alap(output.graph_id)
+
+        # move all operations ALAP
+        for step in reversed(schedule.sfg.get_precedence_list()):
+            for outport in step:
+                if not isinstance(outport.operation, Delay):
+                    schedule.move_operation_alap(outport.operation.graph_id)
+
+
 class ListScheduler(Scheduler, ABC):
     def __init__(
         self,
@@ -75,6 +181,11 @@ class ListScheduler(Scheduler, ABC):
         self._input_times = input_times or {}
         self._output_delta_times = output_delta_times or {}
 
+    @property
+    @abstractmethod
+    def sort_indices(self) -> tuple[tuple[int, bool]]:
+        raise NotImplementedError
+
     def apply_scheduling(self, schedule: "Schedule") -> None:
         """Applies the scheduling algorithm on the given Schedule.
 
@@ -85,6 +196,11 @@ class ListScheduler(Scheduler, ABC):
         """
         sfg = schedule.sfg
 
+        alap_schedule = copy.copy(schedule)
+        ALAPScheduler().apply_scheduling(alap_schedule)
+        alap_start_times = alap_schedule.start_times
+        schedule.start_times = {}
+
         used_resources_ready_times = {}
         remaining_resources = self._max_resources.copy()
         if Input.type_name() not in remaining_resources:
@@ -92,75 +208,94 @@ class ListScheduler(Scheduler, ABC):
         if Output.type_name() not in remaining_resources:
             remaining_resources[Output.type_name()] = 1
 
-        sorted_operations = self._get_sorted_operations(schedule)
+        remaining_ops = (
+            sfg.operations
+            + sfg.find_by_type_name(Input.type_name())
+            + sfg.find_by_type_name(Output.type_name())
+        )
+        remaining_ops = [op.graph_id for op in remaining_ops]
 
         schedule.start_times = {}
-
         remaining_reads = self._max_concurrent_reads
 
         # initial input placement
         if self._input_times:
             for input_id in self._input_times:
                 schedule.start_times[input_id] = self._input_times[input_id]
-            sorted_operations = [
-                elem for elem in sorted_operations if not elem.startswith("in")
+            remaining_ops = [
+                elem for elem in remaining_ops if not elem.startswith("in")
             ]
 
-        current_time = 0
-        timeout_counter = 0
-        while sorted_operations:
+        remaining_ops = [op for op in remaining_ops if not op.startswith("dontcare")]
+        remaining_ops = [op for op in remaining_ops if not op.startswith("t")]
 
-            # generate the best schedulable candidate
-            candidate = sfg.find_by_id(sorted_operations[0])
-            counter = 0
-            while not self._candidate_is_schedulable(
+        current_time = 0
+        time_out_counter = 0
+        while remaining_ops:
+            ready_ops_priority_table = self._get_ready_ops_priority_table(
+                sfg,
                 schedule.start_times,
+                current_time,
+                alap_start_times,
+                remaining_ops,
+                remaining_resources,
+                remaining_reads,
+            )
+            while ready_ops_priority_table:
+                next_op = sfg.find_by_id(self._get_next_op_id(ready_ops_priority_table))
+
+                if next_op.type_name() in remaining_resources:
+                    remaining_resources[next_op.type_name()] -= 1
+                    if (
+                        next_op.type_name() == Input.type_name()
+                        or next_op.type_name() == Output.type_name()
+                    ):
+                        used_resources_ready_times[next_op] = current_time + 1
+                    else:
+                        used_resources_ready_times[next_op] = (
+                            current_time + next_op.execution_time
+                        )
+                remaining_reads -= next_op.input_count
+
+                remaining_ops = [
+                    op_id for op_id in remaining_ops if op_id != next_op.graph_id
+                ]
+                schedule.start_times[next_op.graph_id] = current_time
+
+                ready_ops_priority_table = self._get_ready_ops_priority_table(
+                    sfg,
+                    schedule.start_times,
+                    current_time,
+                    alap_start_times,
+                    remaining_ops,
+                    remaining_resources,
+                    remaining_reads,
+                )
+
+            current_time += 1
+            time_out_counter += 1
+            if time_out_counter >= 100:
+                raise TimeoutError(
+                    "Algorithm did not schedule any operation for 10 time steps, "
+                    "try relaxing constraints."
+                )
+
+            ready_ops_priority_table = self._get_ready_ops_priority_table(
                 sfg,
-                candidate,
+                schedule.start_times,
                 current_time,
+                alap_start_times,
+                remaining_ops,
                 remaining_resources,
                 remaining_reads,
-                self._max_concurrent_writes,
-                sorted_operations,
-            ):
-                if counter == len(sorted_operations):
-                    counter = 0
-                    current_time += 1
-                    timeout_counter += 1
-
-                    if timeout_counter > 10:
-                        msg = "Algorithm did not schedule any operation for 10 time steps, try relaxing constraints."
-                        raise TimeoutError(msg)
+            )
+            # update available reads and operators
+            remaining_reads = self._max_concurrent_reads
+            for operation, ready_time in used_resources_ready_times.items():
+                if ready_time == current_time:
+                    remaining_resources[operation.type_name()] += 1
 
-                    remaining_reads = self._max_concurrent_reads
-
-                    # update available operators
-                    for operation, ready_time in used_resources_ready_times.items():
-                        if ready_time == current_time:
-                            remaining_resources[operation.type_name()] += 1
-
-                else:
-                    candidate = sfg.find_by_id(sorted_operations[counter])
-                    counter += 1
-
-            timeout_counter = 0
-            # if the resource is constrained, update remaining resources
-            if candidate.type_name() in remaining_resources:
-                remaining_resources[candidate.type_name()] -= 1
-                if (
-                    candidate.type_name() == Input.type_name()
-                    or candidate.type_name() == Output.type_name()
-                ):
-                    used_resources_ready_times[candidate] = current_time + 1
-                else:
-                    used_resources_ready_times[candidate] = (
-                        current_time + candidate.execution_time
-                    )
-            remaining_reads -= candidate.input_count
-
-            # schedule the best candidate to the current time
-            sorted_operations.remove(candidate.graph_id)
-            schedule.start_times[candidate.graph_id] = current_time
+        current_time -= 1
 
         self._handle_outputs(schedule)
 
@@ -173,16 +308,92 @@ class ListScheduler(Scheduler, ABC):
 
         schedule.remove_delays()
 
-        # move all dont cares ALAP
-        for dc_op in schedule.sfg.find_by_type_name(DontCare.type_name()):
+        # schedule all dont cares ALAP
+        for dc_op in sfg.find_by_type_name(DontCare.type_name()):
             dc_op = cast(DontCare, dc_op)
+            schedule.start_times[dc_op.graph_id] = 0
             schedule.move_operation_alap(dc_op.graph_id)
 
+    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
+            )
+
+        sorted_table = sorted(ready_ops_priority_table, key=sort_key)
+        return sorted_table[0][0]
+
+    def _get_ready_ops_priority_table(
+        self,
+        sfg: "SFG",
+        start_times: dict["GraphID", int],
+        current_time: int,
+        alap_start_times: dict["GraphID", int],
+        remaining_ops: list["GraphID"],
+        remaining_resources: dict["GraphID", int],
+        remaining_reads: int,
+    ) -> list[tuple["GraphID", int, int, int]]:
+
+        ready_ops = [
+            op_id
+            for op_id in remaining_ops
+            if self._op_is_schedulable(
+                start_times,
+                sfg,
+                sfg.find_by_id(op_id),
+                current_time,
+                remaining_resources,
+                remaining_reads,
+                self._max_concurrent_writes,
+                remaining_ops,
+            )
+        ]
+
+        deadlines = self._calculate_deadlines(sfg, alap_start_times)
+        output_slacks = self._calculate_alap_output_slacks(
+            current_time, alap_start_times
+        )
+        fan_outs = self._calculate_fan_outs(sfg, alap_start_times)
+
+        ready_ops_priority_table = []
+        for op_id in ready_ops:
+            ready_ops_priority_table.append(
+                (op_id, deadlines[op_id], output_slacks[op_id], fan_outs[op_id])
+            )
+        return ready_ops_priority_table
+
+    def _calculate_deadlines(
+        self, sfg, alap_start_times: dict["GraphID", int]
+    ) -> dict["GraphID", int]:
+        return {
+            op_id: start_time + sfg.find_by_id(op_id).latency
+            for op_id, start_time in alap_start_times.items()
+        }
+
+    def _calculate_alap_output_slacks(
+        self, current_time: int, alap_start_times: dict["GraphID", int]
+    ) -> dict["GraphID", int]:
+        return {
+            op_id: start_time - current_time
+            for op_id, start_time in alap_start_times.items()
+        }
+
+    def _calculate_fan_outs(
+        self, sfg: "SFG", alap_start_times: dict["GraphID", int]
+    ) -> dict["GraphID", int]:
+        return {
+            op_id: len(sfg.find_by_id(op_id).output_signals)
+            for op_id, start_time in alap_start_times.items()
+        }
+
     @staticmethod
-    def _candidate_is_schedulable(
+    def _op_is_schedulable(
         start_times: dict["GraphID"],
         sfg: "SFG",
-        operation: "Operation",
+        op: "Operation",
         current_time: int,
         remaining_resources: dict["GraphID", int],
         remaining_reads: int,
@@ -190,12 +401,12 @@ class ListScheduler(Scheduler, ABC):
         remaining_ops: list["GraphID"],
     ) -> bool:
         if (
-            operation.type_name() in remaining_resources
-            and remaining_resources[operation.type_name()] == 0
+            op.type_name() in remaining_resources
+            and remaining_resources[op.type_name()] == 0
         ):
             return False
 
-        op_finish_time = current_time + operation.latency
+        op_finish_time = current_time + op.latency
         future_ops = [
             sfg.find_by_id(item[0])
             for item in start_times.items()
@@ -205,16 +416,16 @@ class ListScheduler(Scheduler, ABC):
         future_ops_writes = sum([op.input_count for op in future_ops])
 
         if (
-            not operation.graph_id.startswith("out")
+            not op.graph_id.startswith("out")
             and future_ops_writes >= max_concurrent_writes
         ):
             return False
 
         read_counter = 0
         earliest_start_time = 0
-        for op_input in operation.inputs:
+        for op_input in op.inputs:
             source_op = op_input.signals[0].source.operation
-            if isinstance(source_op, Delay):
+            if isinstance(source_op, Delay) or isinstance(source_op, DontCare):
                 continue
 
             source_op_graph_id = source_op.graph_id
@@ -235,10 +446,6 @@ class ListScheduler(Scheduler, ABC):
 
         return earliest_start_time <= current_time
 
-    @abstractmethod
-    def _get_sorted_operations(schedule: "Schedule") -> list["GraphID"]:
-        raise NotImplementedError
-
     def _handle_outputs(
         self, schedule: "Schedule", non_schedulable_ops: Optional[list["GraphID"]] = []
     ) -> None:
diff --git a/b_asic/signal_flow_graph.py b/b_asic/signal_flow_graph.py
index 74f5698f..903ab2af 100644
--- a/b_asic/signal_flow_graph.py
+++ b/b_asic/signal_flow_graph.py
@@ -29,7 +29,6 @@ from typing import (
 import numpy as np
 from graphviz import Digraph
 
-from b_asic.core_schedulers import ASAPScheduler
 from b_asic.graph_component import GraphComponent
 from b_asic.operation import (
     AbstractOperation,
@@ -1708,6 +1707,7 @@ class SFG(AbstractOperation):
         """Return the time of the critical path."""
         # Import here needed to avoid circular imports
         from b_asic.schedule import Schedule
+        from b_asic.scheduler import ASAPScheduler
 
         return Schedule(self, ASAPScheduler()).schedule_time
 
diff --git a/examples/auto_scheduling_with_custom_io_times.py b/examples/auto_scheduling_with_custom_io_times.py
index 8913bfd8..c981f467 100644
--- a/examples/auto_scheduling_with_custom_io_times.py
+++ b/examples/auto_scheduling_with_custom_io_times.py
@@ -6,8 +6,9 @@ Auto Scheduling With Custom IO times
 """
 
 from b_asic.core_operations import Butterfly, ConstantMultiplication
-from b_asic.core_schedulers import ASAPScheduler, HybridScheduler
+from b_asic.list_schedulers import HybridScheduler
 from b_asic.schedule import Schedule
+from b_asic.scheduler import ASAPScheduler
 from b_asic.sfg_generators import radix_2_dif_fft
 
 sfg = radix_2_dif_fft(points=8)
diff --git a/examples/fivepointwinograddft.py b/examples/fivepointwinograddft.py
index 856dcb6c..d2156151 100644
--- a/examples/fivepointwinograddft.py
+++ b/examples/fivepointwinograddft.py
@@ -13,8 +13,8 @@ import networkx as nx
 
 from b_asic.architecture import Architecture, Memory, ProcessingElement
 from b_asic.core_operations import AddSub, Butterfly, ConstantMultiplication
-from b_asic.core_schedulers import ASAPScheduler
 from b_asic.schedule import Schedule
+from b_asic.scheduler import ASAPScheduler
 from b_asic.signal_flow_graph import SFG
 from b_asic.special_operations import Input, Output
 
diff --git a/examples/folding_example_with_architecture.py b/examples/folding_example_with_architecture.py
index 66e40299..43bdc798 100644
--- a/examples/folding_example_with_architecture.py
+++ b/examples/folding_example_with_architecture.py
@@ -16,8 +16,8 @@ shorter than the scheduling period.
 
 from b_asic.architecture import Architecture, Memory, ProcessingElement
 from b_asic.core_operations import Addition, ConstantMultiplication
-from b_asic.core_schedulers import ASAPScheduler
 from b_asic.schedule import Schedule
+from b_asic.scheduler import ASAPScheduler
 from b_asic.signal_flow_graph import SFG
 from b_asic.special_operations import Delay, Input, Output
 
diff --git a/examples/ldlt_matrix_inverse.py b/examples/ldlt_matrix_inverse.py
index cf5961aa..ffe74c83 100644
--- a/examples/ldlt_matrix_inverse.py
+++ b/examples/ldlt_matrix_inverse.py
@@ -7,15 +7,14 @@ LDLT Matrix Inversion Algorithm
 
 from b_asic.architecture import Architecture, Memory, ProcessingElement
 from b_asic.core_operations import MADS, DontCare, Reciprocal
-from b_asic.core_schedulers import (
-    ALAPScheduler,
-    ASAPScheduler,
+from b_asic.list_schedulers import (
     EarliestDeadlineScheduler,
     HybridScheduler,
     LeastSlackTimeScheduler,
     MaxFanOutScheduler,
 )
 from b_asic.schedule import Schedule
+from b_asic.scheduler import ALAPScheduler, ASAPScheduler
 from b_asic.sfg_generators import ldlt_matrix_inverse
 from b_asic.special_operations import Input, Output
 
diff --git a/examples/lwdfallpass.py b/examples/lwdfallpass.py
index 7eb78ca7..6bbde629 100644
--- a/examples/lwdfallpass.py
+++ b/examples/lwdfallpass.py
@@ -8,8 +8,8 @@ This has different latency offsets for the different inputs/outputs.
 """
 
 from b_asic.core_operations import SymmetricTwoportAdaptor
-from b_asic.core_schedulers import ASAPScheduler
 from b_asic.schedule import Schedule
+from b_asic.scheduler import ASAPScheduler
 from b_asic.signal_flow_graph import SFG
 from b_asic.special_operations import Delay, Input, Output
 
diff --git a/examples/memory_constrained_scheduling.py b/examples/memory_constrained_scheduling.py
index c1f81fae..a4719cc0 100644
--- a/examples/memory_constrained_scheduling.py
+++ b/examples/memory_constrained_scheduling.py
@@ -7,8 +7,9 @@ Memory Constrained Scheduling
 
 from b_asic.architecture import Architecture, Memory, ProcessingElement
 from b_asic.core_operations import Butterfly, ConstantMultiplication
-from b_asic.core_schedulers import ASAPScheduler, HybridScheduler
+from b_asic.list_schedulers import HybridScheduler
 from b_asic.schedule import Schedule
+from b_asic.scheduler import ASAPScheduler
 from b_asic.sfg_generators import radix_2_dif_fft
 from b_asic.special_operations import Input, Output
 
diff --git a/examples/secondorderdirectformiir.py b/examples/secondorderdirectformiir.py
index 772d8fb8..aa84c26b 100644
--- a/examples/secondorderdirectformiir.py
+++ b/examples/secondorderdirectformiir.py
@@ -6,8 +6,8 @@ Second-order IIR Filter with Schedule
 """
 
 from b_asic.core_operations import Addition, ConstantMultiplication
-from b_asic.core_schedulers import ASAPScheduler
 from b_asic.schedule import Schedule
+from b_asic.scheduler import ASAPScheduler
 from b_asic.signal_flow_graph import SFG
 from b_asic.special_operations import Delay, Input, Output
 
diff --git a/examples/secondorderdirectformiir_architecture.py b/examples/secondorderdirectformiir_architecture.py
index 2147961f..a7d72fb3 100644
--- a/examples/secondorderdirectformiir_architecture.py
+++ b/examples/secondorderdirectformiir_architecture.py
@@ -7,8 +7,8 @@ Second-order IIR Filter with Architecture
 
 from b_asic.architecture import Architecture, Memory, ProcessingElement
 from b_asic.core_operations import Addition, ConstantMultiplication
-from b_asic.core_schedulers import ASAPScheduler
 from b_asic.schedule import Schedule
+from b_asic.scheduler import ASAPScheduler
 from b_asic.signal_flow_graph import SFG
 from b_asic.special_operations import Delay, Input, Output
 
diff --git a/examples/thirdorderblwdf.py b/examples/thirdorderblwdf.py
index 0b815731..d29fd215 100644
--- a/examples/thirdorderblwdf.py
+++ b/examples/thirdorderblwdf.py
@@ -10,8 +10,8 @@ import numpy as np
 from mplsignal.freq_plots import freqz_fir
 
 from b_asic.core_operations import Addition, SymmetricTwoportAdaptor
-from b_asic.core_schedulers import ASAPScheduler
 from b_asic.schedule import Schedule
+from b_asic.scheduler import ASAPScheduler
 from b_asic.signal_flow_graph import SFG
 from b_asic.signal_generator import Impulse
 from b_asic.simulation import Simulation
diff --git a/examples/threepointwinograddft.py b/examples/threepointwinograddft.py
index 5dc962e6..7e7a58ac 100644
--- a/examples/threepointwinograddft.py
+++ b/examples/threepointwinograddft.py
@@ -11,8 +11,8 @@ import networkx as nx
 
 from b_asic.architecture import Architecture, Memory, ProcessingElement
 from b_asic.core_operations import AddSub, ConstantMultiplication
-from b_asic.core_schedulers import ASAPScheduler
 from b_asic.schedule import Schedule
+from b_asic.scheduler import ASAPScheduler
 from b_asic.signal_flow_graph import SFG
 from b_asic.special_operations import Input, Output
 
diff --git a/test/fixtures/schedule.py b/test/fixtures/schedule.py
index b5e9d0fb..39b375ed 100644
--- a/test/fixtures/schedule.py
+++ b/test/fixtures/schedule.py
@@ -1,8 +1,8 @@
 import pytest
 
 from b_asic.core_operations import Addition, ConstantMultiplication
-from b_asic.core_schedulers import ASAPScheduler
 from b_asic.schedule import Schedule
+from b_asic.scheduler import ASAPScheduler
 from b_asic.signal_flow_graph import SFG
 
 
diff --git a/test/integration/test_sfg_to_architecture.py b/test/integration/test_sfg_to_architecture.py
index c07e9766..3bc47504 100644
--- a/test/integration/test_sfg_to_architecture.py
+++ b/test/integration/test_sfg_to_architecture.py
@@ -8,8 +8,9 @@ from b_asic.core_operations import (
     DontCare,
     Reciprocal,
 )
-from b_asic.core_schedulers import ASAPScheduler, HybridScheduler
+from b_asic.list_schedulers import HybridScheduler
 from b_asic.schedule import Schedule
+from b_asic.scheduler import ASAPScheduler
 from b_asic.sfg_generators import ldlt_matrix_inverse, radix_2_dif_fft
 from b_asic.special_operations import Input, Output
 
diff --git a/test/unit/test_architecture.py b/test/unit/test_architecture.py
index 432f0652..ce2d7bcf 100644
--- a/test/unit/test_architecture.py
+++ b/test/unit/test_architecture.py
@@ -6,10 +6,10 @@ import pytest
 
 from b_asic.architecture import Architecture, Memory, ProcessingElement
 from b_asic.core_operations import Addition, ConstantMultiplication
-from b_asic.core_schedulers import ASAPScheduler
 from b_asic.process import PlainMemoryVariable
 from b_asic.resources import ProcessCollection
 from b_asic.schedule import Schedule
+from b_asic.scheduler import ASAPScheduler
 from b_asic.special_operations import Input, Output
 
 
diff --git a/test/unit/test_core_schedulers.py b/test/unit/test_list_schedulers.py
similarity index 75%
rename from test/unit/test_core_schedulers.py
rename to test/unit/test_list_schedulers.py
index 755f5cc6..136e285a 100644
--- a/test/unit/test_core_schedulers.py
+++ b/test/unit/test_list_schedulers.py
@@ -9,9 +9,7 @@ from b_asic.core_operations import (
     ConstantMultiplication,
     Reciprocal,
 )
-from b_asic.core_schedulers import (
-    ALAPScheduler,
-    ASAPScheduler,
+from b_asic.list_schedulers import (
     EarliestDeadlineScheduler,
     HybridScheduler,
     LeastSlackTimeScheduler,
@@ -26,266 +24,6 @@ from b_asic.sfg_generators import (
 from b_asic.special_operations import Input, Output
 
 
-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_1_iir(self):
-        sfg = direct_form_1_iir([1, 2, 3], [1, 2, 3])
-
-        sfg.set_latency_of_type(ConstantMultiplication.type_name(), 2)
-        sfg.set_execution_time_of_type(ConstantMultiplication.type_name(), 1)
-        sfg.set_latency_of_type(Addition.type_name(), 3)
-        sfg.set_execution_time_of_type(Addition.type_name(), 1)
-
-        schedule = Schedule(sfg, scheduler=ASAPScheduler())
-
-        assert schedule.start_times == {
-            "in0": 0,
-            "cmul0": 0,
-            "cmul1": 0,
-            "cmul2": 0,
-            "cmul3": 0,
-            "cmul4": 0,
-            "add3": 2,
-            "add1": 2,
-            "add0": 5,
-            "add2": 8,
-            "out0": 11,
-        }
-        assert schedule.schedule_time == 11
-
-    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
-
-    def test_radix_2_fft_8_points(self):
-        sfg = radix_2_dif_fft(points=8)
-
-        sfg.set_latency_of_type(ConstantMultiplication.type_name(), 2)
-        sfg.set_execution_time_of_type(ConstantMultiplication.type_name(), 1)
-        sfg.set_latency_of_type(Butterfly.type_name(), 1)
-        sfg.set_execution_time_of_type(Butterfly.type_name(), 1)
-
-        schedule = Schedule(sfg, scheduler=ASAPScheduler())
-
-        assert schedule.start_times == {
-            "in0": 0,
-            "in1": 0,
-            "in2": 0,
-            "in3": 0,
-            "in4": 0,
-            "in5": 0,
-            "in6": 0,
-            "in7": 0,
-            "bfly0": 0,
-            "bfly6": 0,
-            "bfly8": 0,
-            "bfly11": 0,
-            "cmul3": 1,
-            "bfly7": 1,
-            "cmul2": 1,
-            "bfly1": 1,
-            "cmul0": 1,
-            "cmul4": 2,
-            "bfly9": 2,
-            "bfly5": 3,
-            "bfly2": 3,
-            "out0": 3,
-            "out4": 3,
-            "bfly10": 4,
-            "cmul1": 4,
-            "bfly3": 4,
-            "out1": 5,
-            "out2": 5,
-            "out5": 5,
-            "out6": 5,
-            "bfly4": 6,
-            "out3": 7,
-            "out7": 7,
-        }
-        assert schedule.schedule_time == 7
-
-
-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_1_iir(self):
-        sfg = direct_form_1_iir([1, 2, 3], [1, 2, 3])
-
-        sfg.set_latency_of_type(ConstantMultiplication.type_name(), 2)
-        sfg.set_execution_time_of_type(ConstantMultiplication.type_name(), 1)
-        sfg.set_latency_of_type(Addition.type_name(), 3)
-        sfg.set_execution_time_of_type(Addition.type_name(), 1)
-
-        schedule = Schedule(sfg, scheduler=ALAPScheduler())
-
-        assert schedule.start_times == {
-            "cmul3": 0,
-            "cmul4": 0,
-            "add1": 2,
-            "in0": 3,
-            "cmul0": 3,
-            "cmul1": 3,
-            "cmul2": 3,
-            "add3": 5,
-            "add0": 5,
-            "add2": 8,
-            "out0": 11,
-        }
-        assert schedule.schedule_time == 11
-
-    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
-
-    def test_radix_2_fft_8_points(self):
-        sfg = radix_2_dif_fft(points=8)
-
-        sfg.set_latency_of_type(ConstantMultiplication.type_name(), 2)
-        sfg.set_execution_time_of_type(ConstantMultiplication.type_name(), 1)
-        sfg.set_latency_of_type(Butterfly.type_name(), 1)
-        sfg.set_execution_time_of_type(Butterfly.type_name(), 1)
-
-        schedule = Schedule(sfg, scheduler=ALAPScheduler())
-
-        assert schedule.start_times == {
-            "in3": 0,
-            "in7": 0,
-            "in1": 0,
-            "in5": 0,
-            "bfly6": 0,
-            "bfly8": 0,
-            "cmul2": 1,
-            "cmul3": 1,
-            "in2": 2,
-            "in6": 2,
-            "bfly11": 2,
-            "bfly7": 3,
-            "cmul0": 3,
-            "bfly5": 3,
-            "in0": 4,
-            "in4": 4,
-            "cmul4": 4,
-            "cmul1": 4,
-            "bfly0": 4,
-            "bfly1": 5,
-            "bfly2": 5,
-            "bfly9": 6,
-            "bfly10": 6,
-            "bfly3": 6,
-            "bfly4": 6,
-            "out0": 7,
-            "out1": 7,
-            "out2": 7,
-            "out3": 7,
-            "out4": 7,
-            "out5": 7,
-            "out6": 7,
-            "out7": 7,
-        }
-        assert schedule.schedule_time == 7
-
-
 class TestEarliestDeadlineScheduler:
     def test_empty_sfg(self, sfg_empty):
         with pytest.raises(
@@ -313,14 +51,14 @@ class TestEarliestDeadlineScheduler:
 
         assert schedule.start_times == {
             "in0": 0,
-            "cmul4": 0,
-            "cmul3": 1,
+            "cmul3": 0,
+            "cmul4": 1,
             "cmul0": 2,
             "add1": 3,
             "cmul1": 3,
             "cmul2": 4,
-            "add3": 6,
-            "add0": 7,
+            "add0": 6,
+            "add3": 7,
             "add2": 10,
             "out0": 13,
         }
@@ -351,8 +89,8 @@ class TestEarliestDeadlineScheduler:
         )
         assert schedule.start_times == {
             "in0": 0,
-            "cmul4": 0,
-            "cmul3": 1,
+            "cmul3": 0,
+            "cmul4": 1,
             "cmul1": 2,
             "cmul2": 3,
             "add1": 4,
@@ -487,14 +225,14 @@ class TestLeastSlackTimeScheduler:
 
         assert schedule.start_times == {
             "in0": 0,
-            "cmul4": 0,
-            "cmul3": 1,
+            "cmul3": 0,
+            "cmul4": 1,
             "cmul0": 2,
             "add1": 3,
             "cmul1": 3,
             "cmul2": 4,
-            "add3": 6,
-            "add0": 7,
+            "add0": 6,
+            "add3": 7,
             "add2": 10,
             "out0": 13,
         }
@@ -525,8 +263,8 @@ class TestLeastSlackTimeScheduler:
         )
         assert schedule.start_times == {
             "in0": 0,
-            "cmul4": 0,
-            "cmul3": 1,
+            "cmul3": 0,
+            "cmul4": 1,
             "cmul1": 2,
             "cmul2": 3,
             "add1": 4,
@@ -657,8 +395,8 @@ class TestMaxFanOutScheduler:
             "cmul0": 0,
             "cmul1": 1,
             "cmul2": 2,
-            "cmul4": 3,
-            "cmul3": 4,
+            "cmul3": 3,
+            "cmul4": 4,
             "add3": 4,
             "add1": 6,
             "add0": 9,
@@ -667,6 +405,57 @@ class TestMaxFanOutScheduler:
         }
         assert schedule.schedule_time == 15
 
+    def test_ldlt_inverse_3x3(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}
+        schedule = Schedule(sfg, scheduler=MaxFanOutScheduler(resources))
+
+        assert schedule.start_times == {
+            "in1": 0,
+            "in2": 1,
+            "in0": 2,
+            "rec0": 2,
+            "in4": 3,
+            "in5": 4,
+            "mads1": 4,
+            "dontcare4": 4,
+            "dontcare5": 5,
+            "in3": 5,
+            "mads0": 5,
+            "mads13": 7,
+            "mads12": 8,
+            "mads14": 9,
+            "rec1": 12,
+            "mads8": 14,
+            "dontcare2": 14,
+            "mads10": 17,
+            "rec2": 20,
+            "mads9": 22,
+            "out5": 22,
+            "dontcare0": 22,
+            "dontcare1": 23,
+            "mads11": 23,
+            "mads7": 25,
+            "out4": 25,
+            "mads3": 26,
+            "mads6": 27,
+            "dontcare3": 27,
+            "out3": 28,
+            "mads2": 29,
+            "out2": 29,
+            "mads5": 30,
+            "mads4": 33,
+            "out1": 33,
+            "out0": 36,
+        }
+        assert schedule.schedule_time == 36
+
 
 class TestHybridScheduler:
     def test_empty_sfg(self, sfg_empty):
@@ -688,14 +477,14 @@ class TestHybridScheduler:
 
         assert schedule.start_times == {
             "in0": 0,
-            "cmul4": 0,
-            "cmul3": 1,
+            "cmul3": 0,
+            "cmul4": 1,
             "cmul0": 2,
             "add1": 3,
             "cmul1": 3,
             "cmul2": 4,
-            "add3": 6,
-            "add0": 7,
+            "add0": 6,
+            "add3": 7,
             "add2": 10,
             "out0": 13,
         }
@@ -797,13 +586,13 @@ class TestHybridScheduler:
             "bfly3": 5,
             "out0": 5,
             "bfly4": 6,
-            "out1": 6,
-            "out2": 12,
-            "out3": 11,
+            "out2": 6,
+            "out3": 7,
+            "out7": 8,
+            "out6": 9,
             "out4": 10,
-            "out5": 9,
-            "out6": 8,
-            "out7": 7,
+            "out1": 11,
+            "out5": 12,
         }
         assert schedule.schedule_time == 12
 
@@ -871,9 +660,9 @@ class TestHybridScheduler:
             "bfly5": 12,
             "cmul4": 13,
             "bfly9": 13,
-            "bfly10": 15,
+            "bfly3": 15,
             "cmul1": 15,
-            "bfly3": 16,
+            "bfly10": 16,
             "bfly4": 17,
             "out0": 18,
             "out1": 19,
@@ -945,9 +734,9 @@ class TestHybridScheduler:
             "bfly5": 12,
             "cmul4": 13,
             "bfly9": 13,
-            "bfly10": 15,
+            "bfly3": 15,
             "cmul1": 15,
-            "bfly3": 16,
+            "bfly10": 16,
             "bfly4": 17,
             "out0": 18,
             "out1": 19,
diff --git a/test/unit/test_schedule.py b/test/unit/test_schedule.py
index 456d53f4..bbd98c36 100644
--- a/test/unit/test_schedule.py
+++ b/test/unit/test_schedule.py
@@ -8,9 +8,9 @@ import matplotlib.testing.decorators
 import pytest
 
 from b_asic.core_operations import Addition, Butterfly, ConstantMultiplication
-from b_asic.core_schedulers import ALAPScheduler, ASAPScheduler
 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.signal_flow_graph import SFG
 from b_asic.special_operations import Delay, Input, Output
diff --git a/test/unit/test_scheduler.py b/test/unit/test_scheduler.py
new file mode 100644
index 00000000..9ec682b2
--- /dev/null
+++ b/test/unit/test_scheduler.py
@@ -0,0 +1,266 @@
+import pytest
+
+from b_asic.core_operations import Addition, Butterfly, ConstantMultiplication
+from b_asic.schedule import Schedule
+from b_asic.scheduler import ALAPScheduler, ASAPScheduler
+from b_asic.sfg_generators import direct_form_1_iir, radix_2_dif_fft
+
+
+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_1_iir(self):
+        sfg = direct_form_1_iir([1, 2, 3], [1, 2, 3])
+
+        sfg.set_latency_of_type(ConstantMultiplication.type_name(), 2)
+        sfg.set_execution_time_of_type(ConstantMultiplication.type_name(), 1)
+        sfg.set_latency_of_type(Addition.type_name(), 3)
+        sfg.set_execution_time_of_type(Addition.type_name(), 1)
+
+        schedule = Schedule(sfg, scheduler=ASAPScheduler())
+
+        assert schedule.start_times == {
+            "in0": 0,
+            "cmul0": 0,
+            "cmul1": 0,
+            "cmul2": 0,
+            "cmul3": 0,
+            "cmul4": 0,
+            "add3": 2,
+            "add1": 2,
+            "add0": 5,
+            "add2": 8,
+            "out0": 11,
+        }
+        assert schedule.schedule_time == 11
+
+    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
+
+    def test_radix_2_fft_8_points(self):
+        sfg = radix_2_dif_fft(points=8)
+
+        sfg.set_latency_of_type(ConstantMultiplication.type_name(), 2)
+        sfg.set_execution_time_of_type(ConstantMultiplication.type_name(), 1)
+        sfg.set_latency_of_type(Butterfly.type_name(), 1)
+        sfg.set_execution_time_of_type(Butterfly.type_name(), 1)
+
+        schedule = Schedule(sfg, scheduler=ASAPScheduler())
+
+        assert schedule.start_times == {
+            "in0": 0,
+            "in1": 0,
+            "in2": 0,
+            "in3": 0,
+            "in4": 0,
+            "in5": 0,
+            "in6": 0,
+            "in7": 0,
+            "bfly0": 0,
+            "bfly6": 0,
+            "bfly8": 0,
+            "bfly11": 0,
+            "cmul3": 1,
+            "bfly7": 1,
+            "cmul2": 1,
+            "bfly1": 1,
+            "cmul0": 1,
+            "cmul4": 2,
+            "bfly9": 2,
+            "bfly5": 3,
+            "bfly2": 3,
+            "out0": 3,
+            "out4": 3,
+            "bfly10": 4,
+            "cmul1": 4,
+            "bfly3": 4,
+            "out1": 5,
+            "out2": 5,
+            "out5": 5,
+            "out6": 5,
+            "bfly4": 6,
+            "out3": 7,
+            "out7": 7,
+        }
+        assert schedule.schedule_time == 7
+
+
+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_1_iir(self):
+        sfg = direct_form_1_iir([1, 2, 3], [1, 2, 3])
+
+        sfg.set_latency_of_type(ConstantMultiplication.type_name(), 2)
+        sfg.set_execution_time_of_type(ConstantMultiplication.type_name(), 1)
+        sfg.set_latency_of_type(Addition.type_name(), 3)
+        sfg.set_execution_time_of_type(Addition.type_name(), 1)
+
+        schedule = Schedule(sfg, scheduler=ALAPScheduler())
+
+        assert schedule.start_times == {
+            "cmul3": 0,
+            "cmul4": 0,
+            "add1": 2,
+            "in0": 3,
+            "cmul0": 3,
+            "cmul1": 3,
+            "cmul2": 3,
+            "add3": 5,
+            "add0": 5,
+            "add2": 8,
+            "out0": 11,
+        }
+        assert schedule.schedule_time == 11
+
+    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
+
+    def test_radix_2_fft_8_points(self):
+        sfg = radix_2_dif_fft(points=8)
+
+        sfg.set_latency_of_type(ConstantMultiplication.type_name(), 2)
+        sfg.set_execution_time_of_type(ConstantMultiplication.type_name(), 1)
+        sfg.set_latency_of_type(Butterfly.type_name(), 1)
+        sfg.set_execution_time_of_type(Butterfly.type_name(), 1)
+
+        schedule = Schedule(sfg, scheduler=ALAPScheduler())
+
+        assert schedule.start_times == {
+            "in3": 0,
+            "in7": 0,
+            "in1": 0,
+            "in5": 0,
+            "bfly6": 0,
+            "bfly8": 0,
+            "cmul2": 1,
+            "cmul3": 1,
+            "in2": 2,
+            "in6": 2,
+            "bfly11": 2,
+            "bfly7": 3,
+            "cmul0": 3,
+            "bfly5": 3,
+            "in0": 4,
+            "in4": 4,
+            "cmul4": 4,
+            "cmul1": 4,
+            "bfly0": 4,
+            "bfly1": 5,
+            "bfly2": 5,
+            "bfly9": 6,
+            "bfly10": 6,
+            "bfly3": 6,
+            "bfly4": 6,
+            "out0": 7,
+            "out1": 7,
+            "out2": 7,
+            "out3": 7,
+            "out4": 7,
+            "out5": 7,
+            "out6": 7,
+            "out7": 7,
+        }
+        assert schedule.schedule_time == 7
diff --git a/test/unit/test_scheduler_gui.py b/test/unit/test_scheduler_gui.py
index 99e84850..02e9d36e 100644
--- a/test/unit/test_scheduler_gui.py
+++ b/test/unit/test_scheduler_gui.py
@@ -1,8 +1,8 @@
 import pytest
 
 from b_asic.core_operations import Addition, ConstantMultiplication
-from b_asic.core_schedulers import ASAPScheduler
 from b_asic.schedule import Schedule
+from b_asic.scheduler import ASAPScheduler
 
 try:
     from b_asic.scheduler_gui.main_window import ScheduleMainWindow
-- 
GitLab