From cf8a00c5b85fd557178e08d720cd34c09513f4fc Mon Sep 17 00:00:00 2001
From: Simon Bjurek <simbj106@student.liu.se>
Date: Mon, 3 Mar 2025 15:15:57 +0000
Subject: [PATCH] Misc fixes, added method for getting min resources, now
 checks iteration bound

---
 b_asic/schedule.py                            |  5 +-
 b_asic/scheduler.py                           | 61 +++++++++++--------
 b_asic/signal_flow_graph.py                   | 27 ++++++++
 .../auto_scheduling_with_custom_io_times.py   | 26 +++++---
 test/unit/test_list_schedulers.py             | 48 +++++++++++++++
 test/unit/test_sfg.py                         | 57 +++++++++++++++++
 6 files changed, 192 insertions(+), 32 deletions(-)

diff --git a/b_asic/schedule.py b/b_asic/schedule.py
index 2c3910de..c62c5f2e 100644
--- a/b_asic/schedule.py
+++ b/b_asic/schedule.py
@@ -29,6 +29,7 @@ from b_asic._preferences import (
     SIGNAL_LINEWIDTH,
     SPLINE_OFFSET,
 )
+from b_asic.core_operations import DontCare
 from b_asic.graph_component import GraphID
 from b_asic.operation import Operation
 from b_asic.port import InputPort, OutputPort
@@ -349,7 +350,9 @@ class Schedule:
         usage_time = start_time + cast(int, input_port.latency_offset)
         for signal in input_port.signals:
             source = cast(OutputPort, signal.source)
-            if source.operation.graph_id.startswith("dontcare"):
+            if isinstance(source.operation, DontCare):
+                available_time = 0
+            elif isinstance(source.operation, Delay):
                 available_time = 0
             else:
                 if self._schedule_time is not None:
diff --git a/b_asic/scheduler.py b/b_asic/scheduler.py
index 0e9fa628..89edd46b 100644
--- a/b_asic/scheduler.py
+++ b/b_asic/scheduler.py
@@ -1,7 +1,6 @@
 import copy
 import sys
 from abc import ABC, abstractmethod
-from math import ceil
 from typing import TYPE_CHECKING, cast
 
 import b_asic.logger as logger
@@ -294,18 +293,21 @@ class ListScheduler(Scheduler, ABC):
                     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():
-            total_exec_time = sum(
-                [op.execution_time for op in self._sfg.find_by_type_name(resource_type)]
-            )
-            if self._schedule.schedule_time is not None:
-                resource_lower_bound = ceil(
-                    total_exec_time / self._schedule.schedule_time
+        if self._schedule._cyclic:
+            if self._schedule.schedule_time is None:
+                raise ValueError("Scheduling time must be provided when cyclic = True.")
+            iteration_period_bound = self._sfg.iteration_period_bound()
+            if self._schedule.schedule_time < iteration_period_bound:
+                raise ValueError(
+                    f"Provided scheduling time {self._schedule.schedule_time} must be larger or equal to the"
+                    f" iteration period bound: {iteration_period_bound}."
                 )
-                if resource_amount < resource_lower_bound:
+
+        if self._schedule.schedule_time is not None:
+            for resource_type, resource_amount in self._max_resources.items():
+                if resource_amount < self._sfg.resource_lower_bound(
+                    resource_type, self._schedule.schedule_time
+                ):
                     raise ValueError(
                         f"Amount of resource: {resource_type} is not enough to "
                         f"realize schedule for scheduling time: {self._schedule.schedule_time}."
@@ -447,6 +449,7 @@ class ListScheduler(Scheduler, ABC):
             for op_id in self._remaining_ops
             if self._op_is_schedulable(self._sfg.find_by_id(op_id))
         ]
+        memory_reads = self._calculate_memory_reads(ready_ops)
 
         return [
             (
@@ -454,6 +457,7 @@ class ListScheduler(Scheduler, ABC):
                 self._deadlines[op_id],
                 self._output_slacks[op_id],
                 self._fan_outs[op_id],
+                memory_reads[op_id],
             )
             for op_id in ready_ops
         ]
@@ -479,6 +483,27 @@ class ListScheduler(Scheduler, ABC):
             for op_id, start_time in alap_start_times.items()
         }
 
+    def _calculate_memory_reads(
+        self, ready_ops: list["GraphID"]
+    ) -> dict["GraphID", int]:
+        op_reads = {}
+        for op_id in ready_ops:
+            reads = 0
+            for op_input in self._sfg.find_by_id(op_id).inputs:
+                source_op = op_input.signals[0].source.operation
+                if isinstance(source_op, DontCare):
+                    continue
+                if isinstance(source_op, Delay):
+                    reads += 1
+                    continue
+                if (
+                    self._schedule.start_times[source_op.graph_id]
+                    != self._current_time - 1
+                ):
+                    reads += 1
+            op_reads[op_id] = reads
+        return op_reads
+
     def _op_satisfies_resource_constraints(self, op: "Operation") -> bool:
         if self._schedule.schedule_time is not None:
             time_slot = self._current_time % self._schedule.schedule_time
@@ -580,18 +605,6 @@ class ListScheduler(Scheduler, ABC):
                     if time == new_time and op_id.startswith("out"):
                         count += 1
 
-                self._remaining_resources = self._max_resources
-                self._remaining_resources[Output.type_name()] -= count
-
-                self._current_time = new_time
-                if not self._op_is_schedulable(output):
-                    raise ValueError(
-                        "Cannot schedule outputs according to the provided output_delta_times. "
-                        f"Failed output: {output.graph_id}, "
-                        f"at time: { self._schedule.start_times[output.graph_id]}, "
-                        "try relaxing the constraints."
-                    )
-
                 modulo_time = (
                     new_time % self._schedule.schedule_time
                     if self._schedule.schedule_time
diff --git a/b_asic/signal_flow_graph.py b/b_asic/signal_flow_graph.py
index abc380ec..839536b2 100644
--- a/b_asic/signal_flow_graph.py
+++ b/b_asic/signal_flow_graph.py
@@ -10,6 +10,7 @@ import warnings
 from collections import defaultdict, deque
 from collections.abc import Iterable, MutableSet, Sequence
 from io import StringIO
+from math import ceil
 from numbers import Number
 from queue import PriorityQueue
 from typing import (
@@ -1729,6 +1730,32 @@ class SFG(AbstractOperation):
                     continue
                 fringe.append((next_state, path + [next_state]))
 
+    def resource_lower_bound(self, type_name: str, schedule_time: int) -> int:
+        """Return the lowest amount of resources of the given type needed to reach the scheduling time.
+
+        Parameters
+        ----------
+        type_name : str
+            Type name of the given resource.
+        schedule_time : int
+            Scheduling time to evaluate for.
+        """
+        ops = self.find_by_type_name(type_name)
+        if not ops:
+            return 0
+        if schedule_time <= 0:
+            raise ValueError(
+                f"Schedule time must be positive, current schedule time is: {schedule_time}."
+            )
+        exec_times = [op.execution_time for op in ops]
+        if any(time is None for time in exec_times):
+            raise ValueError(
+                f"Execution times not set for all operations of type {type_name}."
+            )
+
+        total_exec_time = sum([op.execution_time for op in ops])
+        return ceil(total_exec_time / schedule_time)
+
     def iteration_period_bound(self) -> int:
         """
         Return the iteration period bound of the SFG.
diff --git a/examples/auto_scheduling_with_custom_io_times.py b/examples/auto_scheduling_with_custom_io_times.py
index 2064b9ec..f85710a0 100644
--- a/examples/auto_scheduling_with_custom_io_times.py
+++ b/examples/auto_scheduling_with_custom_io_times.py
@@ -15,7 +15,7 @@ points = 8
 sfg = radix_2_dif_fft(points=points)
 
 # %%
-# The SFG is
+# The SFG is:
 sfg
 
 # %%
@@ -26,7 +26,7 @@ sfg.set_execution_time_of_type(Butterfly.type_name(), 1)
 sfg.set_execution_time_of_type(ConstantMultiplication.type_name(), 1)
 
 # %%
-# Generate an ASAP schedule for reference
+# Generate an ASAP schedule for reference.
 schedule = Schedule(sfg, scheduler=ASAPScheduler())
 schedule.show()
 
@@ -46,8 +46,7 @@ schedule = Schedule(
 schedule.show()
 
 # %%
-# Generate a new Schedule with cyclic scheduling enabled
-output_delta_times = {f"out{i}": i for i in range(points)}
+# Generate a new Schedule with cyclic scheduling enabled.
 schedule = Schedule(
     sfg,
     scheduler=HybridScheduler(
@@ -61,8 +60,7 @@ schedule = Schedule(
 schedule.show()
 
 # %%
-# Generate a new Schedule with even less scheduling time
-output_delta_times = {f"out{i}": i + 1 for i in range(points)}
+# Generate a new Schedule with even less scheduling time.
 schedule = Schedule(
     sfg,
     scheduler=HybridScheduler(
@@ -76,7 +74,21 @@ schedule = Schedule(
 schedule.show()
 
 # %%
-# Try scheduling for 12 cycles, which gives full butterfly usage
+# Try scheduling for 12 cycles, which gives full butterfly usage.
+schedule = Schedule(
+    sfg,
+    scheduler=HybridScheduler(
+        resources,
+        input_times=input_times,
+        output_delta_times=output_delta_times,
+    ),
+    schedule_time=12,
+    cyclic=True,
+)
+schedule.show()
+
+# %%
+# Push output times one step to prevent lap for out3.
 output_delta_times = {f"out{i}": i + 2 for i in range(points)}
 schedule = Schedule(
     sfg,
diff --git a/test/unit/test_list_schedulers.py b/test/unit/test_list_schedulers.py
index fa349f87..b7cd272a 100644
--- a/test/unit/test_list_schedulers.py
+++ b/test/unit/test_list_schedulers.py
@@ -1395,6 +1395,30 @@ class TestHybridScheduler:
                 schedule_time=5,
             )
 
+        sfg = radix_2_dif_fft(points=8)
+
+        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,
+        }
+        with pytest.raises(
+            ValueError,
+            match="Amount of resource: bfly is not enough to realize schedule for scheduling time: 6.",
+        ):
+            Schedule(
+                sfg,
+                scheduler=HybridScheduler(
+                    resources, max_concurrent_reads=2, max_concurrent_writes=2
+                ),
+                schedule_time=6,
+                cyclic=True,
+            )
+
     def test_scheduling_time_not_enough(self):
         sfg = ldlt_matrix_inverse(N=3)
 
@@ -1550,3 +1574,27 @@ class TestHybridScheduler:
                 ),
                 schedule_time=5,
             )
+
+    def test_iteration_period_bound(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)
+
+        resources = {
+            Addition.type_name(): 1,
+            ConstantMultiplication.type_name(): 1,
+        }
+
+        with pytest.raises(
+            ValueError,
+            match="Provided scheduling time 5 must be larger or equal to the iteration period bound: 8.",
+        ):
+            Schedule(
+                sfg,
+                scheduler=EarliestDeadlineScheduler(max_resources=resources),
+                schedule_time=5,
+                cyclic=True,
+            )
diff --git a/test/unit/test_sfg.py b/test/unit/test_sfg.py
index bc4e93a9..d042d071 100644
--- a/test/unit/test_sfg.py
+++ b/test/unit/test_sfg.py
@@ -1811,6 +1811,63 @@ class TestInsertDelays:
         assert source4.type_name() == bfly.type_name()
 
 
+class TestResourceLowerBound:
+    def test_empty_sfg(self):
+        sfg = SFG()
+        assert sfg.resource_lower_bound("add", 2) == 0
+        assert sfg.resource_lower_bound("cmul", 1000) == 0
+
+    def test_type_not_in_sfg(self, sfg_simple_accumulator):
+        sfg_simple_accumulator.resource_lower_bound("bfly", 2) == 0
+        sfg_simple_accumulator.resource_lower_bound("bfly", 1000) == 0
+
+    def test_negative_schedule_time(self, precedence_sfg_delays):
+        precedence_sfg_delays.set_latency_of_type("add", 2)
+        precedence_sfg_delays.set_latency_of_type("cmul", 3)
+        precedence_sfg_delays.set_execution_time_of_type("add", 1)
+        precedence_sfg_delays.set_execution_time_of_type("cmul", 1)
+
+        with pytest.raises(
+            ValueError,
+            match="Schedule time must be positive, current schedule time is: 0.",
+        ):
+            precedence_sfg_delays.resource_lower_bound("add", 0)
+
+        with pytest.raises(
+            ValueError,
+            match="Schedule time must be positive, current schedule time is: -1.",
+        ):
+            precedence_sfg_delays.resource_lower_bound("cmul", -1)
+
+    def test_accumulator(self, sfg_simple_accumulator):
+        sfg_simple_accumulator.set_latency_of_type('add', 2)
+
+        with pytest.raises(
+            ValueError,
+            match="Execution times not set for all operations of type add.",
+        ):
+            sfg_simple_accumulator.resource_lower_bound("add", 2)
+
+        sfg_simple_accumulator.set_execution_time_of_type("add", 2)
+        assert sfg_simple_accumulator.resource_lower_bound("add", 2) == 1
+        assert sfg_simple_accumulator.resource_lower_bound("add", 1) == 2
+
+    def test_secondorder_iir(self, precedence_sfg_delays):
+        precedence_sfg_delays.set_latency_of_type("add", 2)
+        precedence_sfg_delays.set_latency_of_type("cmul", 3)
+
+        precedence_sfg_delays.set_execution_time_of_type("add", 1)
+        assert precedence_sfg_delays.resource_lower_bound("add", 1) == 4
+        assert precedence_sfg_delays.resource_lower_bound("add", 2) == 2
+        assert precedence_sfg_delays.resource_lower_bound("add", 4) == 1
+
+        precedence_sfg_delays.set_execution_time_of_type("cmul", 1)
+        assert precedence_sfg_delays.resource_lower_bound("cmul", 1) == 7
+        assert precedence_sfg_delays.resource_lower_bound("cmul", 2) == 4
+        assert precedence_sfg_delays.resource_lower_bound("cmul", 4) == 2
+        assert precedence_sfg_delays.resource_lower_bound("cmul", 7) == 1
+
+
 class TestIterationPeriodBound:
     def test_accumulator(self, sfg_simple_accumulator):
         sfg_simple_accumulator.set_latency_of_type('add', 2)
-- 
GitLab