From afb4d8cb1a174011029af9f8fe085a0fa828ad10 Mon Sep 17 00:00:00 2001
From: Oscar Gustafsson <oscar.gustafsson@gmail.com>
Date: Sun, 4 Jun 2023 14:20:24 +0200
Subject: [PATCH] Test schedule and some fixes

---
 b_asic/schedule.py    |  40 +++++-----
 test/test_schedule.py | 182 ++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 193 insertions(+), 29 deletions(-)

diff --git a/b_asic/schedule.py b/b_asic/schedule.py
index a842a8b3..2603afcb 100644
--- a/b_asic/schedule.py
+++ b/b_asic/schedule.py
@@ -152,7 +152,7 @@ class Schedule:
             The graph id of the operation to get the start time for.
         """
         if graph_id not in self._start_times:
-            raise ValueError(f"No operation with graph_id {graph_id} in schedule")
+            raise ValueError(f"No operation with graph_id {graph_id!r} in schedule")
         return self._start_times[graph_id]
 
     def get_max_end_time(self) -> int:
@@ -188,7 +188,7 @@ class Schedule:
         slacks
         """
         if graph_id not in self._start_times:
-            raise ValueError(f"No operation with graph_id {graph_id} in schedule")
+            raise ValueError(f"No operation with graph_id {graph_id!r} in schedule")
         output_slacks = self._forward_slacks(graph_id)
         return cast(
             int,
@@ -256,7 +256,7 @@ class Schedule:
         slacks
         """
         if graph_id not in self._start_times:
-            raise ValueError(f"No operation with graph_id {graph_id} in schedule")
+            raise ValueError(f"No operation with graph_id {graph_id!r} in schedule")
         input_slacks = self._backward_slacks(graph_id)
         return cast(
             int,
@@ -322,7 +322,7 @@ class Schedule:
         forward_slack
         """
         if graph_id not in self._start_times:
-            raise ValueError(f"No operation with graph_id {graph_id} in schedule")
+            raise ValueError(f"No operation with graph_id {graph_id!r} in schedule")
         return self.backward_slack(graph_id), self.forward_slack(graph_id)
 
     def print_slacks(self, order: int = 0) -> None:
@@ -614,12 +614,14 @@ class Schedule:
             The time to move. If positive move forward, if negative move backward.
         """
         if graph_id not in self._start_times:
-            raise ValueError(f"No operation with graph_id {graph_id} in schedule")
+            raise ValueError(f"No operation with graph_id {graph_id!r} in schedule")
 
+        if time == 0:
+            return self
         (backward_slack, forward_slack) = self.slacks(graph_id)
         if not -backward_slack <= time <= forward_slack:
             raise ValueError(
-                f"Operation {graph_id} got incorrect move: {time}. Must be"
+                f"Operation {graph_id!r} got incorrect move: {time}. Must be"
                 f" between {-backward_slack} and {forward_slack}."
             )
 
@@ -685,7 +687,6 @@ class Schedule:
         ):
             new_start = self._schedule_time
             self._laps[op.input(0).signals[0].graph_id] -= 1
-            print(f"Moved {graph_id}")
         # Set new start time
         self._start_times[graph_id] = new_start
         return self
@@ -698,22 +699,21 @@ class Schedule:
 
             schedule.move_operation(graph_id, schedule.forward_slack(graph_id))
 
-        but Outputs will only move to the end of the schedule.
+        but operations with no succeeding operation (Outputs) will only move to the end
+        of the schedule.
 
         Parameters
         ----------
         graph_id : GraphID
             The graph id of the operation to move.
         """
-        op = self._sfg.find_by_id(graph_id)
-        if op is None:
-            raise ValueError(f"No operation with graph_id {graph_id!r} in schedule")
-        if isinstance(op, Output):
+        forward_slack = self.forward_slack(graph_id)
+        if forward_slack == sys.maxsize:
             self.move_operation(
                 graph_id, self.schedule_time - self._start_times[graph_id]
             )
         else:
-            self.move_operation(graph_id, self.forward_slack(graph_id))
+            self.move_operation(graph_id, forward_slack)
         return self
 
     def move_operation_asap(self, graph_id: GraphID) -> "Schedule":
@@ -724,20 +724,19 @@ class Schedule:
 
             schedule.move_operation(graph_id, -schedule.backward_slack(graph_id))
 
-        but Inputs will only move to the start of the schedule.
+        but operations that do not have a preceeding operation (Inputs and Constants)
+        will only move to the start of the schedule.
 
         Parameters
         ----------
         graph_id : GraphID
             The graph id of the operation to move.
         """
-        op = self._sfg.find_by_id(graph_id)
-        if op is None:
-            raise ValueError(f"No operation with graph_id {graph_id!r} in schedule")
-        if isinstance(op, Input):
+        backward_slack = self.backward_slack(graph_id)
+        if backward_slack == sys.maxsize:
             self.move_operation(graph_id, -self._start_times[graph_id])
         else:
-            self.move_operation(graph_id, -self.backward_slack(graph_id))
+            self.move_operation(graph_id, -backward_slack)
         return self
 
     def _remove_delays_no_laps(self) -> None:
@@ -789,8 +788,7 @@ class Schedule:
         precedence_list = self._sfg.get_precedence_list()
 
         if len(precedence_list) < 2:
-            print("Empty signal flow graph cannot be scheduled.")
-            return
+            raise ValueError("Empty signal flow graph cannot be scheduled.")
 
         non_schedulable_ops = set()
         for outport in precedence_list[0]:
diff --git a/test/test_schedule.py b/test/test_schedule.py
index c9e6c038..c7305ecb 100644
--- a/test/test_schedule.py
+++ b/test/test_schedule.py
@@ -27,15 +27,17 @@ class TestInit:
         }
         assert schedule.schedule_time == 9
 
+        with pytest.raises(
+            ValueError, match="No operation with graph_id 'foo' in schedule"
+        ):
+            schedule.start_time_of_operation("foo")
+
     def test_complicated_single_outputs_normal_latency(self, precedence_sfg_delays):
         precedence_sfg_delays.set_latency_of_type(Addition.type_name(), 4)
         precedence_sfg_delays.set_latency_of_type(ConstantMultiplication.type_name(), 3)
 
         schedule = Schedule(precedence_sfg_delays, algorithm="ASAP")
 
-        for op in schedule._sfg.get_operations_topological_order():
-            print(op.latency_offsets)
-
         start_times_names = {}
         for op_id, start_time in schedule._start_times.items():
             op_name = precedence_sfg_delays.find_by_id(op_id).name
@@ -66,9 +68,6 @@ class TestInit:
 
         schedule = Schedule(precedence_sfg_delays, algorithm="ALAP")
 
-        for op in schedule._sfg.get_operations_topological_order():
-            print(op.latency_offsets)
-
         start_times_names = {}
         for op_id in schedule.start_times:
             op_name = precedence_sfg_delays.find_by_id(op_id).name
@@ -91,6 +90,44 @@ class TestInit:
         }
         assert schedule.schedule_time == 21
 
+    def test_complicated_single_outputs_normal_latency_alap_with_schedule_time(
+        self, precedence_sfg_delays
+    ):
+        precedence_sfg_delays.set_latency_of_type(Addition.type_name(), 4)
+        precedence_sfg_delays.set_latency_of_type(ConstantMultiplication.type_name(), 3)
+
+        schedule = Schedule(precedence_sfg_delays, schedule_time=25, algorithm="ALAP")
+
+        start_times_names = {}
+        for op_id in schedule.start_times:
+            op_name = precedence_sfg_delays.find_by_id(op_id).name
+            start_times_names[op_name] = schedule.start_time_of_operation(op_id)
+
+        assert start_times_names == {
+            "IN1": 8,
+            "C0": 8,
+            "B1": 4,
+            "B2": 4,
+            "ADD2": 7,
+            "ADD1": 11,
+            "Q1": 15,
+            "A0": 18,
+            "A1": 14,
+            "A2": 14,
+            "ADD3": 17,
+            "ADD4": 21,
+            "OUT1": 25,
+        }
+        assert schedule.schedule_time == 25
+
+    def test_complicated_single_outputs_normal_latency_alap_too_short_schedule_time(
+        self, precedence_sfg_delays
+    ):
+        precedence_sfg_delays.set_latency_of_type(Addition.type_name(), 4)
+        precedence_sfg_delays.set_latency_of_type(ConstantMultiplication.type_name(), 3)
+        with pytest.raises(ValueError, match="Too short schedule time. Minimum is 21."):
+            Schedule(precedence_sfg_delays, schedule_time=19, algorithm="ALAP")
+
     def test_complicated_single_outputs_normal_latency_from_fixture(
         self, secondorder_iir_schedule
     ):
@@ -247,6 +284,74 @@ class TestSlacks:
             precedence_sfg_delays.find_by_name("A2")[0].graph_id
         ) == (16, 0)
 
+    def test_print_slacks(self, capsys, precedence_sfg_delays):
+        precedence_sfg_delays.set_latency_of_type(Addition.type_name(), 1)
+        precedence_sfg_delays.set_latency_of_type(ConstantMultiplication.type_name(), 3)
+
+        schedule = Schedule(precedence_sfg_delays, algorithm="ASAP")
+        schedule.print_slacks()
+        captured = capsys.readouterr()
+        assert captured.out == """Graph ID | Backward |  Forward
+---------|----------|---------
+add1     |        0 |        0
+add2     |        0 |        0
+add3     |        0 |        0
+add4     |        0 |        7
+cmul1    |        0 |        1
+cmul2    |        0 |        0
+cmul3    |        0 |        0
+cmul4    |        4 |        0
+cmul5    |       16 |        0
+cmul6    |       16 |        0
+cmul7    |        4 |        0
+in1      |       oo |        0
+out1     |        0 |       oo
+"""
+        assert captured.err == ""
+
+    def test_print_slacks_sorting(self, capsys, precedence_sfg_delays):
+        precedence_sfg_delays.set_latency_of_type(Addition.type_name(), 1)
+        precedence_sfg_delays.set_latency_of_type(ConstantMultiplication.type_name(), 3)
+
+        schedule = Schedule(precedence_sfg_delays, algorithm="ASAP")
+        schedule.print_slacks(1)
+        captured = capsys.readouterr()
+        assert captured.out == """Graph ID | Backward |  Forward
+---------|----------|---------
+cmul1    |        0 |        1
+add1     |        0 |        0
+add2     |        0 |        0
+cmul2    |        0 |        0
+cmul3    |        0 |        0
+add4     |        0 |        7
+add3     |        0 |        0
+out1     |        0 |       oo
+cmul4    |        4 |        0
+cmul7    |        4 |        0
+cmul5    |       16 |        0
+cmul6    |       16 |        0
+in1      |       oo |        0
+"""
+        assert captured.err == ""
+
+    def test_slacks_errors(self, precedence_sfg_delays):
+        precedence_sfg_delays.set_latency_of_type(Addition.type_name(), 1)
+        precedence_sfg_delays.set_latency_of_type(ConstantMultiplication.type_name(), 3)
+
+        schedule = Schedule(precedence_sfg_delays, algorithm="ASAP")
+        with pytest.raises(
+            ValueError, match="No operation with graph_id 'foo' in schedule"
+        ):
+            schedule.forward_slack("foo")
+        with pytest.raises(
+            ValueError, match="No operation with graph_id 'foo' in schedule"
+        ):
+            schedule.backward_slack("foo")
+        with pytest.raises(
+            ValueError, match="No operation with graph_id 'foo' in schedule"
+        ):
+            schedule.slacks("foo")
+
 
 class TestRescheduling:
     def test_move_operation(self, precedence_sfg_delays):
@@ -281,6 +386,11 @@ class TestRescheduling:
             "OUT1": 21,
         }
 
+        with pytest.raises(
+            ValueError, match="No operation with graph_id 'foo' in schedule"
+        ):
+            schedule.move_operation("foo", 0)
+
     def test_move_operation_slack_after_rescheduling(self, precedence_sfg_delays):
         precedence_sfg_delays.set_latency_of_type(Addition.type_name(), 1)
         precedence_sfg_delays.set_latency_of_type(ConstantMultiplication.type_name(), 3)
@@ -310,7 +420,7 @@ class TestRescheduling:
         schedule = Schedule(precedence_sfg_delays, algorithm="ASAP")
         with pytest.raises(
             ValueError,
-            match="Operation add4 got incorrect move: -4. Must be between 0 and 7.",
+            match="Operation 'add4' got incorrect move: -4. Must be between 0 and 7.",
         ):
             schedule.move_operation(
                 precedence_sfg_delays.find_by_name("ADD3")[0].graph_id, -4
@@ -323,12 +433,35 @@ class TestRescheduling:
         schedule = Schedule(precedence_sfg_delays, algorithm="ASAP")
         with pytest.raises(
             ValueError,
-            match="Operation add4 got incorrect move: 10. Must be between 0 and 7.",
+            match="Operation 'add4' got incorrect move: 10. Must be between 0 and 7.",
         ):
             schedule.move_operation(
                 precedence_sfg_delays.find_by_name("ADD3")[0].graph_id, 10
             )
 
+    def test_move_operation_asap(self, precedence_sfg_delays):
+        precedence_sfg_delays.set_latency_of_type(Addition.type_name(), 1)
+        precedence_sfg_delays.set_latency_of_type(ConstantMultiplication.type_name(), 3)
+
+        schedule = Schedule(precedence_sfg_delays, algorithm="ASAP")
+        assert schedule.backward_slack('cmul6') == 16
+        assert schedule.forward_slack('cmul6') == 0
+        schedule.move_operation_asap('cmul6')
+        assert schedule.start_time_of_operation('in1') == 0
+        assert schedule.laps['cmul6'] == 0
+        assert schedule.backward_slack('cmul6') == 0
+        assert schedule.forward_slack('cmul6') == 16
+
+    def test_move_input_asap_does_not_mess_up_laps(self, precedence_sfg_delays):
+        precedence_sfg_delays.set_latency_of_type(Addition.type_name(), 1)
+        precedence_sfg_delays.set_latency_of_type(ConstantMultiplication.type_name(), 3)
+
+        schedule = Schedule(precedence_sfg_delays, algorithm="ASAP")
+        old_laps = schedule.laps['in1']
+        schedule.move_operation_asap('in1')
+        assert schedule.start_time_of_operation('in1') == 0
+        assert schedule.laps['in1'] == old_laps
+
     def test_move_operation_acc(self):
         in0 = Input()
         d = Delay()
@@ -594,6 +727,24 @@ class TestErrors:
         ):
             Schedule(sfg_simple_filter, algorithm="foo")
 
+    def test_no_sfg(self):
+        with pytest.raises(TypeError, match="An SFG must be provided"):
+            Schedule(1)
+
+    def test_provided_no_start_times(self, sfg_simple_filter):
+        sfg_simple_filter.set_latency_of_type(Addition.type_name(), 1)
+        sfg_simple_filter.set_latency_of_type(ConstantMultiplication.type_name(), 2)
+        with pytest.raises(
+            ValueError, match="Must provide start_times when using 'provided'"
+        ):
+            Schedule(sfg_simple_filter, algorithm="provided")
+
+    def test_provided_no_laps(self, sfg_simple_filter):
+        sfg_simple_filter.set_latency_of_type(Addition.type_name(), 1)
+        sfg_simple_filter.set_latency_of_type(ConstantMultiplication.type_name(), 2)
+        with pytest.raises(ValueError, match="Must provide laps when using 'provided'"):
+            Schedule(sfg_simple_filter, algorithm="provided", start_times={'in0': 0})
+
 
 class TestGetUsedTypeNames:
     def test_secondorder_iir_schedule(self, secondorder_iir_schedule):
@@ -603,3 +754,18 @@ class TestGetUsedTypeNames:
             'in',
             'out',
         ]
+
+
+class TestYLocations:
+    def test_provided_no_laps(self, sfg_simple_filter):
+        sfg_simple_filter.set_latency_of_type(Addition.type_name(), 1)
+        sfg_simple_filter.set_latency_of_type(ConstantMultiplication.type_name(), 2)
+        schedule = Schedule(sfg_simple_filter)
+        # Assign locations
+        schedule.show()
+        print(schedule._y_locations)
+        assert schedule._y_locations == {'in1': 0, 'cmul1': 1, 'add1': 2, 'out1': 3}
+        schedule.move_y_location('add1', 1, insert=True)
+        assert schedule._y_locations == {'in1': 0, 'cmul1': 2, 'add1': 1, 'out1': 3}
+        schedule.move_y_location('out1', 1)
+        assert schedule._y_locations == {'in1': 0, 'cmul1': 2, 'add1': 1, 'out1': 1}
-- 
GitLab