diff --git a/b_asic/core_operations.py b/b_asic/core_operations.py index 9772d73b72760f76e42ac0ce655de3a2fe379360..b4c9642ba9372f98ccdaa0d5023af6e67720786b 100644 --- a/b_asic/core_operations.py +++ b/b_asic/core_operations.py @@ -294,6 +294,10 @@ class Multiplication(AbstractOperation): Operation execution time (time units before operator can be reused). + See also + ======== + ConstantMultiplication + """ def __init__( @@ -332,6 +336,9 @@ class Division(AbstractOperation): .. math:: y = \frac{x_0}{x_1} + See also + ======== + Reciprocal """ def __init__( @@ -371,6 +378,10 @@ class Min(AbstractOperation): .. math:: y = \min\{x_0 , x_1\} .. note:: Only real-valued numbers are supported. + + See also + ======== + Max """ def __init__( @@ -414,6 +425,10 @@ class Max(AbstractOperation): .. math:: y = \max\{x_0 , x_1\} .. note:: Only real-valued numbers are supported. + + See also + ======== + Min """ def __init__( @@ -745,6 +760,10 @@ class Reciprocal(AbstractOperation): Gives the reciprocal of its input. .. math:: y = \frac{1}{x} + + See also + ======== + Division """ def __init__( diff --git a/b_asic/operation.py b/b_asic/operation.py index 497774224867ffced7230daad1041a2e537118ff..23c5cd99468526c02b649b180bc02a32ebe95d78 100644 --- a/b_asic/operation.py +++ b/b_asic/operation.py @@ -976,17 +976,19 @@ class AbstractOperation(Operation, AbstractGraphComponent): port_str = port_str.lower() if port_str.startswith("in"): index_str = port_str[2:] - assert index_str.isdigit(), ( - "Incorrectly formatted index in string, expected 'in' +" - f" index, got: {port_str!r}" - ) + if not index_str.isdigit(): + raise ValueError( + "Incorrectly formatted index in string, expected 'in'" + f" + index, got: {port_str!r}" + ) self.input(int(index_str)).latency_offset = latency_offset elif port_str.startswith("out"): index_str = port_str[3:] - assert index_str.isdigit(), ( - "Incorrectly formatted index in string, expected 'out' +" - f" index, got: {port_str!r}" - ) + if not index_str.isdigit(): + raise ValueError( + "Incorrectly formatted index in string, expected" + f" 'out' + index, got: {port_str!r}" + ) self.output(int(index_str)).latency_offset = latency_offset else: raise ValueError( diff --git a/b_asic/schedule.py b/b_asic/schedule.py index 6da555cb31e7da9db95e356b14cc27e083d26843..280615606b31b3b98ae42d2380e5cc4efc394807 100644 --- a/b_asic/schedule.py +++ b/b_asic/schedule.py @@ -53,7 +53,7 @@ class Schedule: The schedule time. If not provided, it will be determined by the scheduling algorithm. cyclic : bool, default: False If the schedule is cyclic. - scheduling_alg : {'ASAP'}, optional + scheduling_algorithm : {'ASAP'}, optional The scheduling algorithm to use. Currently, only "ASAP" is supported. """ @@ -69,7 +69,7 @@ class Schedule: sfg: SFG, schedule_time: Optional[int] = None, cyclic: bool = False, - scheduling_alg: str = "ASAP", + scheduling_algorithm: str = "ASAP", ): """Construct a Schedule from an SFG.""" self._sfg = sfg @@ -77,11 +77,11 @@ class Schedule: self._laps = defaultdict(lambda: 0) self._cyclic = cyclic self._y_locations = defaultdict(lambda: None) - if scheduling_alg == "ASAP": + if scheduling_algorithm == "ASAP": self._schedule_asap() else: raise NotImplementedError( - f"No algorithm with name: {scheduling_alg} defined." + f"No algorithm with name: {scheduling_algorithm} defined." ) max_end_time = self.get_max_end_time() @@ -269,8 +269,8 @@ class Schedule: """ if time < self.get_max_end_time(): raise ValueError( - "New schedule time ({time})to short, minimum:" - " ({self.get_max_end_time()})." + f"New schedule time ({time}) too short, minimum:" + f" {self.get_max_end_time()}." ) self._schedule_time = time return self @@ -510,13 +510,16 @@ class Schedule: # Schedule the operation if it does not have a start time yet. op_start_time = 0 for inport in op.inputs: - assert ( - len(inport.signals) == 1 - ), "Error in scheduling, dangling input port detected." - assert inport.signals[0].source is not None, ( - "Error in scheduling, signal with no source" - " detected." - ) + if len(inport.signals) != 1: + raise ValueError( + "Error in scheduling, dangling input port" + " detected." + ) + if inport.signals[0].source is None: + raise ValueError( + "Error in scheduling, signal with no source" + " detected." + ) source_port = inport.signals[0].source source_end_time = None @@ -530,23 +533,24 @@ class Schedule: source_port.operation.graph_id ] - assert source_port.latency_offset is not None, ( - f"Output port: {source_port.index} of" - " operation: " - f" {source_port.operation.graph_id} has no" - " latency-offset." - ) + 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 ) - assert inport.latency_offset is not None, ( - f"Input port: {inport.index} of operation: " - " " - f" {inport.operation.graph_id} has no" - " latency-offset." - ) + if inport.latency_offset is None: + raise ValueError( + f"Input port {inport.index} of operation" + f" {inport.operation.graph_id} has no" + " latency-offset." + ) op_start_time_from_in = ( source_end_time - inport.latency_offset ) @@ -561,6 +565,12 @@ class Schedule: if source_port.operation.graph_id in non_schedulable_ops: self._start_times[output.graph_id] = 0 else: + 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." + ) self._start_times[output.graph_id] = self._start_times[ source_port.operation.graph_id ] + cast(int, source_port.latency_offset) diff --git a/b_asic/scheduler_gui/scheduler_item.py b/b_asic/scheduler_gui/scheduler_item.py index ddea53005c8a39e6ce789cb7a3c18ce1c7e0c0a9..5ff8fa10ff8054164de273d62b2e3fcd84c159ef 100644 --- a/b_asic/scheduler_gui/scheduler_item.py +++ b/b_asic/scheduler_gui/scheduler_item.py @@ -98,7 +98,8 @@ class SchedulerItem(SchedulerEvent, QGraphicsItemGroup): # PySide2 / PyQt5 The x-position to check. """ # TODO: implement - assert self.schedule is not None, "No schedule installed." + if self.schedule is None: + raise ValueError("No schedule installed.") end_time = item.end_time new_start_time = floor(pos) - floor(self._x_axis_indent) slacks = self.schedule.slacks(item.graph_id) @@ -177,7 +178,8 @@ class SchedulerItem(SchedulerEvent, QGraphicsItemGroup): # PySide2 / PyQt5 """ # TODO: implement # item = self.scene().mouseGrabberItem() - assert self.schedule is not None, "No schedule installed." + if self.schedule is None: + raise ValueError("No schedule installed.") return ( self.schedule.schedule_time + delta_time >= self.schedule.get_max_end_time() @@ -187,7 +189,8 @@ class SchedulerItem(SchedulerEvent, QGraphicsItemGroup): # PySide2 / PyQt5 """Change the schedule time by *delta_time* and redraw the graph.""" if self._axes is None: raise RuntimeError("No AxesItem!") - assert self.schedule is not None, "No schedule installed." + if self.schedule is None: + raise ValueError("No schedule installed.") self.schedule.set_schedule_time( self.schedule.schedule_time + delta_time ) diff --git a/b_asic/signal_flow_graph.py b/b_asic/signal_flow_graph.py index af39c97849a1ff1013a821f969f343bbef537656..d111de970fe77454115b6879de4677291851b749 100644 --- a/b_asic/signal_flow_graph.py +++ b/b_asic/signal_flow_graph.py @@ -194,10 +194,11 @@ class SFG(AbstractOperation): Input, self._add_component_unconnected_copy(input_op) ) for signal in input_op.output(0).signals: - assert signal not in self._original_components_to_new, ( - "Duplicate input signals connected to input ports" - " supplied to SFG constructor." - ) + if signal in self._original_components_to_new: + raise ValueError( + "Duplicate input signals connected to input ports" + " supplied to SFG constructor." + ) new_signal = cast( Signal, self._add_component_unconnected_copy(signal) ) @@ -649,15 +650,16 @@ class SFG(AbstractOperation): sfg_copy = self() # Copy to not mess with this SFG. component_copy = sfg_copy.find_by_id(graph_id) - assert component_copy is not None and isinstance( - component_copy, Operation - ), "No operation matching the criteria found" - assert ( - component_copy.output_count == component.output_count - ), "The output count may not differ between the operations" - assert ( - component_copy.input_count == component.input_count - ), "The input count may not differ between the operations" + if component_copy is None or not isinstance(component_copy, Operation): + raise ValueError("No operation matching the criteria found") + if component_copy.output_count != component.output_count: + raise TypeError( + "The output count may not differ between the operations" + ) + if component_copy.input_count != component.input_count: + raise TypeError( + "The input count may not differ between the operations" + ) for index_in, inp in enumerate(component_copy.inputs): for signal in inp.signals: @@ -703,10 +705,11 @@ class SFG(AbstractOperation): f" ({len(output_comp.output_signals)}) does not match input" f" count for component ({component.input_count})." ) - assert len(output_comp.output_signals) == component.output_count, ( - "Destination operation input count does not match output for" - " component." - ) + if len(output_comp.output_signals) != component.output_count: + raise TypeError( + "Destination operation input count does not match output for" + " component." + ) for index, signal_in in enumerate(output_comp.output_signals): destination = cast(InputPort, signal_in.destination) @@ -1063,9 +1066,8 @@ class SFG(AbstractOperation): def _add_component_unconnected_copy( self, original_component: GraphComponent ) -> GraphComponent: - assert ( - original_component not in self._original_components_to_new - ), "Tried to add duplicate SFG component" + if original_component in self._original_components_to_new: + raise ValueError("Tried to add duplicate SFG component") new_component = original_component.copy_component() self._original_components_to_new[original_component] = new_component if ( diff --git a/test/fixtures/schedule.py b/test/fixtures/schedule.py index 40b953179eb962a0661efc90ad0befba4cf4bc7c..15bb0038ec48a219cf465dc64f67ab626769afd9 100644 --- a/test/fixtures/schedule.py +++ b/test/fixtures/schedule.py @@ -13,5 +13,5 @@ def secondorder_iir_schedule(precedence_sfg_delays): ConstantMultiplication.type_name(), 3 ) - schedule = Schedule(precedence_sfg_delays, scheduling_alg="ASAP") + schedule = Schedule(precedence_sfg_delays, scheduling_algorithm="ASAP") return schedule diff --git a/test/test_operation.py b/test/test_operation.py index 67f4278112d8f4f7c9bf91372ad51102715a8264..8f2dbd0afba1e767e6fd8dfdc465af8cd3f6f41b 100644 --- a/test/test_operation.py +++ b/test/test_operation.py @@ -1,6 +1,9 @@ """ B-ASIC test suite for the AbstractOperation class. """ +import re + +import pytest from b_asic import ( MAD, @@ -217,18 +220,6 @@ class TestLatency: "out1": 9, } - def test_set_latency_offsets(self): - bfly = Butterfly() - - bfly.set_latency_offsets({"in0": 3, "out1": 5}) - - assert bfly.latency_offsets == { - "in0": 3, - "in1": None, - "out0": None, - "out1": 5, - } - class TestExecutionTime: def test_execution_time_constructor(self): @@ -240,6 +231,13 @@ class TestExecutionTime: assert bfly.execution_time == 3 + def test_set_execution_time_negative(self): + bfly = Butterfly() + with pytest.raises( + ValueError, match="Execution time cannot be negative" + ): + bfly.execution_time = -1 + class TestCopyOperation: def test_copy_butterfly_latency_offsets(self): @@ -324,3 +322,47 @@ class TestSplit: assert len(split) == 2 assert sum(isinstance(op, Addition) for op in split) == 1 assert sum(isinstance(op, Subtraction) for op in split) == 1 + + +class TestLatencyOffset: + def test_set_latency_offsets(self): + bfly = Butterfly() + + bfly.set_latency_offsets({"in0": 3, "out1": 5}) + + assert bfly.latency_offsets == { + "in0": 3, + "in1": None, + "out0": None, + "out1": 5, + } + + def test_set_latency_offsets_error(self): + bfly = Butterfly() + + with pytest.raises( + ValueError, + match=re.escape( + "Incorrectly formatted index in string, expected 'in' + index," + " got: 'ina'" + ), + ): + bfly.set_latency_offsets({"ina": 3, "out1": 5}) + + with pytest.raises( + ValueError, + match=re.escape( + "Incorrectly formatted index in string, expected 'out' +" + " index, got: 'outb'" + ), + ): + bfly.set_latency_offsets({"in1": 3, "outb": 5}) + + with pytest.raises( + ValueError, + match=re.escape( + "Incorrectly formatted string, expected 'in' + index or 'out'" + " + index, got: 'foo'" + ), + ): + bfly.set_latency_offsets({"foo": 3, "out2": 5}) diff --git a/test/test_schedule.py b/test/test_schedule.py index 6678cfeb2f75562ccdaa98623068661cbce5649a..93199f01ebb6585b71bce4abbf2ecb0e3615584a 100644 --- a/test/test_schedule.py +++ b/test/test_schedule.py @@ -1,9 +1,14 @@ """ B-ASIC test suite for the schedule module and Schedule class. """ +import re + import pytest -from b_asic import Addition, ConstantMultiplication, Schedule +from b_asic.core_operations import Addition, Butterfly, ConstantMultiplication +from b_asic.schedule import Schedule +from b_asic.signal_flow_graph import SFG +from b_asic.special_operations import Input, Output class TestInit: @@ -31,7 +36,7 @@ class TestInit: ConstantMultiplication.type_name(), 3 ) - schedule = Schedule(precedence_sfg_delays, scheduling_alg="ASAP") + schedule = Schedule(precedence_sfg_delays, scheduling_algorithm="ASAP") for op in schedule._sfg.get_operations_topological_order(): print(op.latency_offsets) @@ -122,7 +127,7 @@ class TestInit: {"in0": 6, "in1": 7, "out0": 9} ) - schedule = Schedule(precedence_sfg_delays, scheduling_alg="ASAP") + schedule = Schedule(precedence_sfg_delays, scheduling_algorithm="ASAP") start_times_names = {} for op_id, start_time in schedule._start_times.items(): @@ -152,7 +157,7 @@ class TestInit: ): schedule = Schedule( sfg_two_inputs_two_outputs_independent_with_cmul, - scheduling_alg="ASAP", + scheduling_algorithm="ASAP", ) start_times_names = {} @@ -187,7 +192,7 @@ class TestSlacks: ConstantMultiplication.type_name(), 3 ) - schedule = Schedule(precedence_sfg_delays, scheduling_alg="ASAP") + schedule = Schedule(precedence_sfg_delays, scheduling_algorithm="ASAP") assert ( schedule.forward_slack( precedence_sfg_delays.find_by_name("ADD3")[0].graph_id @@ -220,7 +225,7 @@ class TestSlacks: ConstantMultiplication.type_name(), 3 ) - schedule = Schedule(precedence_sfg_delays, scheduling_alg="ASAP") + schedule = Schedule(precedence_sfg_delays, scheduling_algorithm="ASAP") assert schedule.slacks( precedence_sfg_delays.find_by_name("ADD3")[0].graph_id ) == (0, 7) @@ -236,7 +241,7 @@ class TestRescheduling: ConstantMultiplication.type_name(), 3 ) - schedule = Schedule(precedence_sfg_delays, scheduling_alg="ASAP") + schedule = Schedule(precedence_sfg_delays, scheduling_algorithm="ASAP") schedule.move_operation( precedence_sfg_delays.find_by_name("ADD3")[0].graph_id, 4 @@ -274,7 +279,7 @@ class TestRescheduling: ConstantMultiplication.type_name(), 3 ) - schedule = Schedule(precedence_sfg_delays, scheduling_alg="ASAP") + schedule = Schedule(precedence_sfg_delays, scheduling_algorithm="ASAP") add3_id = precedence_sfg_delays.find_by_name("ADD3")[0].graph_id schedule.move_operation(add3_id, 4) assert schedule.forward_slack(add3_id) == 3 @@ -300,7 +305,7 @@ class TestRescheduling: ConstantMultiplication.type_name(), 3 ) - schedule = Schedule(precedence_sfg_delays, scheduling_alg="ASAP") + schedule = Schedule(precedence_sfg_delays, scheduling_algorithm="ASAP") with pytest.raises(ValueError): schedule.move_operation( precedence_sfg_delays.find_by_name("ADD3")[0].graph_id, -4 @@ -314,7 +319,7 @@ class TestRescheduling: ConstantMultiplication.type_name(), 3 ) - schedule = Schedule(precedence_sfg_delays, scheduling_alg="ASAP") + schedule = Schedule(precedence_sfg_delays, scheduling_algorithm="ASAP") with pytest.raises(ValueError): schedule.move_operation( precedence_sfg_delays.find_by_name("ADD3")[0].graph_id, 10 @@ -327,7 +332,7 @@ class TestTimeResolution: ): schedule = Schedule( sfg_two_inputs_two_outputs_independent_with_cmul, - scheduling_alg="ASAP", + scheduling_algorithm="ASAP", ) old_schedule_time = schedule.schedule_time assert schedule.get_possible_time_resolution_decrements() == [1] @@ -363,7 +368,7 @@ class TestTimeResolution: ): schedule = Schedule( sfg_two_inputs_two_outputs_independent_with_cmul, - scheduling_alg="ASAP", + scheduling_algorithm="ASAP", ) old_schedule_time = schedule.schedule_time @@ -404,7 +409,7 @@ class TestTimeResolution: ): schedule = Schedule( sfg_two_inputs_two_outputs_independent_with_cmul, - scheduling_alg="ASAP", + scheduling_algorithm="ASAP", ) old_schedule_time = schedule.schedule_time assert schedule.get_possible_time_resolution_decrements() == [1] @@ -473,3 +478,72 @@ class TestFigureGeneration: @pytest.mark.mpl_image_compare(remove_text=True, style='mpl20') def test__get_figure_no_execution_times(self, secondorder_iir_schedule): return secondorder_iir_schedule._get_figure() + + +class TestErrors: + def test_no_latency(self, sfg_simple_filter): + with pytest.raises( + ValueError, + match="Input port 0 of operation add1 has no latency-offset.", + ): + Schedule(sfg_simple_filter) + + def test_no_output_latency(self): + in1 = Input() + in2 = Input() + bfly = Butterfly( + in1, in2, latency_offsets={"in0": 4, "in1": 2, "out0": 10} + ) + out1 = Output(bfly.output(0)) + out2 = Output(bfly.output(1)) + sfg = SFG([in1, in2], [out1, out2]) + with pytest.raises( + ValueError, + match="Output port 1 of operation bfly1 has no latency-offset.", + ): + Schedule(sfg) + in1 = Input() + in2 = Input() + bfly1 = Butterfly( + in1, in2, latency_offsets={"in0": 4, "in1": 2, "out1": 10} + ) + bfly2 = Butterfly( + bfly1.output(0), + bfly1.output(1), + latency_offsets={"in0": 4, "in1": 2, "out0": 10, "out1": 8}, + ) + out1 = Output(bfly2.output(0)) + out2 = Output(bfly2.output(1)) + sfg = SFG([in1, in2], [out1, out2]) + with pytest.raises( + ValueError, + match="Output port 0 of operation bfly1 has no latency-offset.", + ): + Schedule(sfg) + + def test_too_short_schedule_time(self, sfg_simple_filter): + sfg_simple_filter.set_latency_of_type(Addition.type_name(), 5) + sfg_simple_filter.set_latency_of_type( + ConstantMultiplication.type_name(), 4 + ) + with pytest.raises( + ValueError, match="Too short schedule time. Minimum is 9." + ): + Schedule(sfg_simple_filter, schedule_time=3) + + schedule = Schedule(sfg_simple_filter) + with pytest.raises( + ValueError, + match=re.escape("New schedule time (3) too short, minimum: 9."), + ): + schedule.set_schedule_time(3) + + def test_incorrect_scheduling_algorithm(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( + NotImplementedError, match="No algorithm with name: foo defined." + ): + Schedule(sfg_simple_filter, scheduling_algorithm="foo") diff --git a/test/test_sfg.py b/test/test_sfg.py index 1531203986911bb257e99188cc48062128b8ba65..00079b7daec11c113de88db23b282b9947279d4d 100644 --- a/test/test_sfg.py +++ b/test/test_sfg.py @@ -329,27 +329,24 @@ class TestReplaceComponents: sfg = SFG(outputs=[Output(large_operation_tree)]) component_id = "addd1" - try: + with pytest.raises( + ValueError, match="No operation matching the criteria found" + ): sfg = sfg.replace_component( Multiplication(name="Multi"), graph_id=component_id ) - except AssertionError: - assert True - else: - assert False def test_not_equal_input(self, large_operation_tree): sfg = SFG(outputs=[Output(large_operation_tree)]) component_id = "c1" - try: + with pytest.raises( + TypeError, + match="The input count may not differ between the operations", + ): sfg = sfg.replace_component( Multiplication(name="Multi"), graph_id=component_id ) - except AssertionError: - assert True - else: - assert False class TestConstructSFG: