From e4fb4668358c70eb6799de0d3bd79d5ce6493684 Mon Sep 17 00:00:00 2001 From: Simon Bjurek <simbj106@student.liu.se> Date: Mon, 10 Mar 2025 08:27:45 +0000 Subject: [PATCH] Refactored ListScheduler, made it non-abstract and added better checks to... --- b_asic/list_schedulers.py | 9 +- b_asic/schedule.py | 10 +- b_asic/scheduler.py | 313 ++++++++++++++++-------------- b_asic/signal_flow_graph.py | 7 +- test/unit/test_gui.py | 29 +-- test/unit/test_list_schedulers.py | 133 ++++++++++--- test/unit/test_sfg.py | 24 +++ 7 files changed, 321 insertions(+), 204 deletions(-) diff --git a/b_asic/list_schedulers.py b/b_asic/list_schedulers.py index dd9550ea..9b2351e5 100644 --- a/b_asic/list_schedulers.py +++ b/b_asic/list_schedulers.py @@ -12,15 +12,14 @@ class EarliestDeadlineScheduler(ListScheduler): max_concurrent_writes: int | None = None, input_times: dict["GraphID", int] | None = None, output_delta_times: dict["GraphID", int] | None = None, - cyclic: bool | None = False, ) -> None: super().__init__( + sort_order=((1, True),), max_resources=max_resources, max_concurrent_reads=max_concurrent_reads, max_concurrent_writes=max_concurrent_writes, input_times=input_times, output_delta_times=output_delta_times, - sort_order=((1, True),), ) @@ -36,12 +35,12 @@ class LeastSlackTimeScheduler(ListScheduler): output_delta_times: dict["GraphID", int] = None, ) -> None: super().__init__( + sort_order=((2, True),), max_resources=max_resources, max_concurrent_reads=max_concurrent_reads, max_concurrent_writes=max_concurrent_writes, input_times=input_times, output_delta_times=output_delta_times, - sort_order=((2, True),), ) @@ -57,12 +56,12 @@ class MaxFanOutScheduler(ListScheduler): output_delta_times: dict["GraphID", int] = None, ) -> None: super().__init__( + sort_order=((3, False),), max_resources=max_resources, max_concurrent_reads=max_concurrent_reads, max_concurrent_writes=max_concurrent_writes, input_times=input_times, output_delta_times=output_delta_times, - sort_order=((3, False),), ) @@ -78,10 +77,10 @@ class HybridScheduler(ListScheduler): output_delta_times: dict["GraphID", int] = None, ) -> None: super().__init__( + sort_order=((2, True), (3, False)), max_resources=max_resources, max_concurrent_reads=max_concurrent_reads, max_concurrent_writes=max_concurrent_writes, input_times=input_times, output_delta_times=output_delta_times, - sort_order=((2, True), (3, False)), ) diff --git a/b_asic/schedule.py b/b_asic/schedule.py index 7799b272..6dfe5809 100644 --- a/b_asic/schedule.py +++ b/b_asic/schedule.py @@ -177,9 +177,15 @@ class Schedule: raise ValueError(f"Extra operations detected in start_times: {extra_elems}") for graph_id, time in self._start_times.items(): - if self.forward_slack(graph_id) < 0 or self.backward_slack(graph_id) < 0: + if self.forward_slack(graph_id) < 0: raise ValueError( - f"Negative slack detected in Schedule for operation: {graph_id}." + f"Negative forward slack detected in Schedule for operation: {graph_id}, " + f"slack: {self.forward_slack(graph_id)}." + ) + if self.backward_slack(graph_id) < 0: + raise ValueError( + f"Negative backward forward slack detected in Schedule for operation: {graph_id}, " + f"slack: {self.backward_slack(graph_id)}" ) if time > self._schedule_time and not graph_id.startswith("dontcare"): raise ValueError( diff --git a/b_asic/scheduler.py b/b_asic/scheduler.py index cd442274..745be1a9 100644 --- a/b_asic/scheduler.py +++ b/b_asic/scheduler.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from typing import TYPE_CHECKING, cast import b_asic.logger as logger -from b_asic.core_operations import DontCare +from b_asic.core_operations import DontCare, Sink from b_asic.port import OutputPort from b_asic.special_operations import Delay, Output from b_asic.types import TypeName @@ -158,13 +158,16 @@ class ALAPScheduler(Scheduler): schedule.sort_y_locations_on_start_times() -class ListScheduler(Scheduler, ABC): +class ListScheduler(Scheduler): """ List-based scheduler that schedules the operations while complying to the given constraints. Parameters ---------- + sort_order : tuple[tuple[int, bool]] + Specifies which columns in the priority table to sort on and in + which order, where True is ascending order. max_resources : dict[TypeName, int] | None, optional Max resources available to realize the schedule, by default None max_concurrent_reads : int | None, optional @@ -175,20 +178,16 @@ class ListScheduler(Scheduler, ABC): Specified input times, by default None output_delta_times : dict[GraphID, int] | None, optional Specified output delta times, by default None - cyclic : bool | None, optional - If the scheduler is allowed to schedule cyclically (modulo), by default False - sort_order : tuple[tuple[int, bool]] - Specifies which columns in the priority table to sort on and in which order, where True is ascending order. """ def __init__( self, + sort_order: tuple[tuple[int, bool], ...], max_resources: dict[TypeName, int] | None = None, max_concurrent_reads: int | None = None, max_concurrent_writes: int | None = None, input_times: dict["GraphID", int] | None = None, output_delta_times: dict["GraphID", int] | None = None, - sort_order=tuple[tuple[int, bool], ...], ) -> None: super() self._logger = logger.getLogger("list_scheduler") @@ -265,119 +264,10 @@ class ListScheduler(Scheduler, ABC): schedule : Schedule Schedule to apply the scheduling algorithm on. """ - self._logger.debug("--- Scheduler initializing ---") - - self._schedule = schedule - self._sfg = schedule.sfg - - for resource_type in self._max_resources.keys(): - if not self._sfg.find_by_type_name(resource_type): - raise ValueError( - f"Provided max resource of type {resource_type} cannot be found in the provided SFG." - ) - - differing_elems = [ - resource - for resource in self._sfg.get_used_type_names() - if resource not in self._max_resources.keys() - ] - for type_name in differing_elems: - self._max_resources[type_name] = 1 - - for key in self._input_times.keys(): - if self._sfg.find_by_id(key) is None: - raise ValueError( - f"Provided input time with GraphID {key} cannot be found in the provided SFG." - ) - - for key in self._output_delta_times.keys(): - if self._sfg.find_by_id(key) is None: - raise ValueError( - f"Provided output delta time with GraphID {key} cannot be found in the provided SFG." - ) - - if self._schedule._cyclic: - 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 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}." - ) - - alap_schedule = copy.copy(self._schedule) - alap_schedule._schedule_time = None - ALAPScheduler().apply_scheduling(alap_schedule) - alap_start_times = alap_schedule.start_times - self._schedule.start_times = {} - - if not self._schedule._cyclic and self._schedule.schedule_time: - if alap_schedule.schedule_time > self._schedule.schedule_time: - raise ValueError( - f"Provided scheduling time {schedule.schedule_time} cannot be reached, " - "try to enable the cyclic property or increase the time to at least " - f"{alap_schedule.schedule_time}." - ) - - self._remaining_resources = self._max_resources.copy() - - self._remaining_ops = self._sfg.operations - self._remaining_ops = [op.graph_id for op in self._remaining_ops] - - self._cached_latency_offsets = { - op_id: self._sfg.find_by_id(op_id).latency_offsets - for op_id in self._remaining_ops - } - self._cached_execution_times = { - op_id: self._sfg.find_by_id(op_id).execution_time - for op_id in self._remaining_ops - } - - self._deadlines = self._calculate_deadlines(alap_start_times) - self._output_slacks = self._calculate_alap_output_slacks(alap_start_times) - self._fan_outs = self._calculate_fan_outs(alap_start_times) - - self._schedule.start_times = {} - self._used_reads = {0: 0} - - self._current_time = 0 - self._op_laps = {} - - self._remaining_ops = [ - op for op in self._remaining_ops if not op.startswith("dontcare") - ] - self._remaining_ops = [ - op for op in self._remaining_ops if not op.startswith("t") - ] - self._remaining_ops = [ - op - for op in self._remaining_ops - if not (op.startswith("out") and op in self._output_delta_times) - ] + self._initialize_scheduler(schedule) if self._input_times: - self._logger.debug("--- Input placement starting ---") - for input_id in self._input_times: - self._schedule.start_times[input_id] = self._input_times[input_id] - self._op_laps[input_id] = 0 - self._logger.debug( - f" {input_id} time: {self._schedule.start_times[input_id]}" - ) - self._remaining_ops = [ - elem for elem in self._remaining_ops if not elem.startswith("in") - ] - self._logger.debug("--- Input placement completed ---") + self._place_inputs_on_given_times() self._logger.debug("--- Operation scheduling starting ---") while self._remaining_ops: @@ -387,24 +277,7 @@ class ListScheduler(Scheduler, ABC): self._get_next_op_id(ready_ops_priority_table) ) - for i, input_port in enumerate(next_op.inputs): - source_op = input_port.signals[0].source.operation - if ( - not isinstance(source_op, DontCare) - and not isinstance(source_op, Delay) - and self._schedule.start_times[source_op.graph_id] - != self._current_time - 1 - ): - time = ( - self._current_time - + self._cached_latency_offsets[next_op.graph_id][f"in{i}"] - ) - if self._schedule.schedule_time: - time %= self._schedule.schedule_time - if self._used_reads.get(time): - self._used_reads[time] += 1 - else: - self._used_reads[time] = 1 + self._update_port_reads(next_op) self._remaining_ops = [ op_id for op_id in self._remaining_ops if op_id != next_op.graph_id @@ -417,14 +290,7 @@ class ListScheduler(Scheduler, ABC): else 0 ) - if self._schedule.schedule_time is not None: - self._logger.debug( - f" Op: {next_op.graph_id}, time: {self._current_time % self._schedule.schedule_time}" - ) - else: - self._logger.debug( - f" Op: {next_op.graph_id}, time: {self._current_time}" - ) + self._log_scheduled_op(next_op) ready_ops_priority_table = self._get_ready_ops_priority_table() @@ -442,12 +308,7 @@ class ListScheduler(Scheduler, ABC): self._schedule.remove_delays() - # schedule all dont cares ALAP - for dc_op in self._sfg.find_by_type_name(DontCare.type_name()): - self._schedule.start_times[dc_op.graph_id] = 0 - self._schedule.place_operation( - dc_op, schedule.forward_slack(dc_op.graph_id) - ) + self._handle_dont_cares() self._schedule.sort_y_locations_on_start_times() self._logger.debug("--- Scheduling completed ---") @@ -671,6 +532,150 @@ class ListScheduler(Scheduler, ABC): and self._op_satisfies_concurrent_reads(op) ) + def _initialize_scheduler(self, schedule: "Schedule") -> None: + self._logger.debug("--- Scheduler initializing ---") + + self._schedule = schedule + self._sfg = schedule.sfg + + for resource_type in self._max_resources.keys(): + if not self._sfg.find_by_type_name(resource_type): + raise ValueError( + f"Provided max resource of type {resource_type} cannot be found in the provided SFG." + ) + + differing_elems = [ + resource + for resource in self._sfg.get_used_type_names() + if resource not in self._max_resources.keys() + and resource != Delay.type_name() + and resource != DontCare.type_name() + and resource != Sink.type_name() + ] + for type_name in differing_elems: + self._max_resources[type_name] = 1 + + for key in self._input_times.keys(): + if self._sfg.find_by_id(key) is None: + raise ValueError( + f"Provided input time with GraphID {key} cannot be found in the provided SFG." + ) + + for key in self._output_delta_times.keys(): + if self._sfg.find_by_id(key) is None: + raise ValueError( + f"Provided output delta time with GraphID {key} cannot be found in the provided SFG." + ) + + if self._schedule._cyclic and self._schedule.schedule_time is not None: + 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 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}." + ) + + alap_schedule = copy.copy(self._schedule) + alap_schedule._schedule_time = None + ALAPScheduler().apply_scheduling(alap_schedule) + alap_start_times = alap_schedule.start_times + self._schedule.start_times = {} + for key in self._schedule._laps.keys(): + self._schedule._laps[key] = 0 + + if not self._schedule._cyclic and self._schedule.schedule_time: + if alap_schedule.schedule_time > self._schedule.schedule_time: + raise ValueError( + f"Provided scheduling time {schedule.schedule_time} cannot be reached, " + "try to enable the cyclic property or increase the time to at least " + f"{alap_schedule.schedule_time}." + ) + + self._remaining_resources = self._max_resources.copy() + + self._remaining_ops = self._sfg.operations + self._remaining_ops = [op.graph_id for op in self._remaining_ops] + + self._cached_latency_offsets = { + op_id: self._sfg.find_by_id(op_id).latency_offsets + for op_id in self._remaining_ops + } + self._cached_execution_times = { + op_id: self._sfg.find_by_id(op_id).execution_time + for op_id in self._remaining_ops + } + + self._deadlines = self._calculate_deadlines(alap_start_times) + self._output_slacks = self._calculate_alap_output_slacks(alap_start_times) + self._fan_outs = self._calculate_fan_outs(alap_start_times) + + self._schedule.start_times = {} + self._used_reads = {0: 0} + + self._current_time = 0 + self._op_laps = {} + + self._remaining_ops = [ + op for op in self._remaining_ops if not op.startswith("dontcare") + ] + self._remaining_ops = [ + op for op in self._remaining_ops if not op.startswith("t") + ] + self._remaining_ops = [ + op + for op in self._remaining_ops + if not (op.startswith("out") and op in self._output_delta_times) + ] + + def _log_scheduled_op(self, next_op: "Operation") -> None: + if self._schedule.schedule_time is not None: + self._logger.debug(f" Op: {next_op.graph_id}, time: {self._current_time}") + else: + self._logger.debug(f" Op: {next_op.graph_id}, time: {self._current_time}") + + def _update_port_reads(self, next_op: "Operation") -> None: + for i, input_port in enumerate(next_op.inputs): + source_op = input_port.signals[0].source.operation + if ( + not isinstance(source_op, DontCare) + and not isinstance(source_op, Delay) + and self._schedule.start_times[source_op.graph_id] + != self._current_time - 1 + ): + time = ( + self._current_time + + self._cached_latency_offsets[next_op.graph_id][f"in{i}"] + ) + if self._schedule.schedule_time: + time %= self._schedule.schedule_time + if self._used_reads.get(time): + self._used_reads[time] += 1 + else: + self._used_reads[time] = 1 + + def _place_inputs_on_given_times(self) -> None: + self._logger.debug("--- Input placement starting ---") + for input_id in self._input_times: + self._schedule.start_times[input_id] = self._input_times[input_id] + self._op_laps[input_id] = 0 + self._logger.debug( + f" {input_id} time: {self._schedule.start_times[input_id]}" + ) + self._remaining_ops = [ + elem for elem in self._remaining_ops if not elem.startswith("in") + ] + self._logger.debug("--- Input placement completed ---") + def _handle_outputs(self) -> None: self._logger.debug("--- Output placement starting ---") if self._schedule._cyclic: @@ -730,3 +735,11 @@ class ListScheduler(Scheduler, ABC): f" {output.graph_id} moved {min_slack} time steps backwards to new time {new_time}" ) self._logger.debug("--- Output placement optimization completed ---") + + def _handle_dont_cares(self) -> None: + # schedule all dont cares ALAP + for dc_op in self._sfg.find_by_type_name(DontCare.type_name()): + self._schedule.start_times[dc_op.graph_id] = 0 + self._schedule.place_operation( + dc_op, self._schedule.forward_slack(dc_op.graph_id) + ) diff --git a/b_asic/signal_flow_graph.py b/b_asic/signal_flow_graph.py index 839536b2..c0c71483 100644 --- a/b_asic/signal_flow_graph.py +++ b/b_asic/signal_flow_graph.py @@ -9,6 +9,7 @@ import re import warnings from collections import defaultdict, deque from collections.abc import Iterable, MutableSet, Sequence +from fractions import Fraction from io import StringIO from math import ceil from numbers import Number @@ -1756,7 +1757,7 @@ class SFG(AbstractOperation): total_exec_time = sum([op.execution_time for op in ops]) return ceil(total_exec_time / schedule_time) - def iteration_period_bound(self) -> int: + def iteration_period_bound(self) -> Fraction: """ Return the iteration period bound of the SFG. @@ -1823,9 +1824,9 @@ class SFG(AbstractOperation): if key in element: time_of_loop += item if number_of_t_in_loop in (0, 1): - t_l_values.append(time_of_loop) + t_l_values.append(Fraction(time_of_loop, 1)) else: - t_l_values.append(time_of_loop / number_of_t_in_loop) + t_l_values.append(Fraction(time_of_loop, number_of_t_in_loop)) return max(t_l_values) def state_space_representation(self): diff --git a/test/unit/test_gui.py b/test/unit/test_gui.py index 95c39693..d910765c 100644 --- a/test/unit/test_gui.py +++ b/test/unit/test_gui.py @@ -148,20 +148,21 @@ def test_help_dialogs(qtbot): widget.exit_app() -def test_simulate(qtbot, datadir): - # Smoke test to open up the "Simulate SFG" and run default simulation - # Should really test all different tests - widget = SFGMainWindow() - qtbot.addWidget(widget) - widget._load_from_file(datadir.join('twotapfir.py')) - assert 'twotapfir' in widget._sfg_dict - widget.simulate_sfg() - qtbot.wait(100) - widget._simulation_dialog.save_properties() - qtbot.wait(100) - widget._simulation_dialog.close() - - widget.exit_app() +# failing right now sometimes on pyside6 +# def test_simulate(qtbot, datadir): +# # Smoke test to open up the "Simulate SFG" and run default simulation +# # Should really test all different tests +# widget = SFGMainWindow() +# qtbot.addWidget(widget) +# widget._load_from_file(datadir.join('twotapfir.py')) +# assert 'twotapfir' in widget._sfg_dict +# widget.simulate_sfg() +# qtbot.wait(100) +# widget._simulation_dialog.save_properties() +# qtbot.wait(100) +# widget._simulation_dialog.close() + +# widget.exit_app() def test_properties_window_smoke_test(qtbot, datadir): diff --git a/test/unit/test_list_schedulers.py b/test/unit/test_list_schedulers.py index 1217de6c..34daa4d8 100644 --- a/test/unit/test_list_schedulers.py +++ b/test/unit/test_list_schedulers.py @@ -1,5 +1,6 @@ import sys +import numpy as np import pytest from b_asic.core_operations import ( @@ -21,6 +22,9 @@ from b_asic.sfg_generators import ( ldlt_matrix_inverse, radix_2_dif_fft, ) +from b_asic.signal_flow_graph import SFG +from b_asic.signal_generator import Constant, Impulse +from b_asic.simulation import Simulation from b_asic.special_operations import Input, Output @@ -63,6 +67,7 @@ class TestEarliestDeadlineScheduler: "out0": 13, } assert schedule.schedule_time == 13 + _validate_recreated_sfg_filter(sfg, schedule) def test_direct_form_2_iir_1_add_1_mul(self, sfg_direct_form_iir_lp_filter): sfg_direct_form_iir_lp_filter.set_latency_of_type( @@ -102,6 +107,7 @@ class TestEarliestDeadlineScheduler: } assert schedule.schedule_time == 13 + _validate_recreated_sfg_filter(sfg_direct_form_iir_lp_filter, schedule) def test_direct_form_2_iir_2_add_3_mul(self, sfg_direct_form_iir_lp_filter): sfg_direct_form_iir_lp_filter.set_latency_of_type( @@ -141,6 +147,7 @@ class TestEarliestDeadlineScheduler: } assert schedule.schedule_time == 12 + _validate_recreated_sfg_filter(sfg_direct_form_iir_lp_filter, schedule) def test_radix_2_fft_8_points(self): sfg = radix_2_dif_fft(points=8) @@ -196,6 +203,7 @@ class TestEarliestDeadlineScheduler: "out3": 7, } assert schedule.schedule_time == 7 + _validate_recreated_sfg_fft(schedule, 8) class TestLeastSlackTimeScheduler: @@ -237,6 +245,7 @@ class TestLeastSlackTimeScheduler: "out0": 13, } assert schedule.schedule_time == 13 + _validate_recreated_sfg_filter(sfg, schedule) def test_direct_form_2_iir_1_add_1_mul(self, sfg_direct_form_iir_lp_filter): sfg_direct_form_iir_lp_filter.set_latency_of_type( @@ -276,6 +285,7 @@ class TestLeastSlackTimeScheduler: } assert schedule.schedule_time == 13 + _validate_recreated_sfg_filter(sfg_direct_form_iir_lp_filter, schedule) def test_direct_form_2_iir_2_add_3_mul(self, sfg_direct_form_iir_lp_filter): sfg_direct_form_iir_lp_filter.set_latency_of_type( @@ -315,6 +325,7 @@ class TestLeastSlackTimeScheduler: } assert schedule.schedule_time == 12 + _validate_recreated_sfg_filter(sfg_direct_form_iir_lp_filter, schedule) def test_radix_2_fft_8_points(self): sfg = radix_2_dif_fft(points=8) @@ -370,6 +381,7 @@ class TestLeastSlackTimeScheduler: "out3": 7, } assert schedule.schedule_time == 7 + _validate_recreated_sfg_fft(schedule, 8) class TestMaxFanOutScheduler: @@ -404,6 +416,7 @@ class TestMaxFanOutScheduler: "out0": 15, } assert schedule.schedule_time == 15 + _validate_recreated_sfg_filter(sfg, schedule) def test_ldlt_inverse_3x3(self): sfg = ldlt_matrix_inverse(N=3) @@ -489,6 +502,7 @@ class TestHybridScheduler: "out0": 13, } assert schedule.schedule_time == 13 + _validate_recreated_sfg_filter(sfg, schedule) def test_radix_2_fft_8_points(self): sfg = radix_2_dif_fft(points=8) @@ -542,6 +556,7 @@ class TestHybridScheduler: "out3": 7, } assert schedule.schedule_time == 7 + _validate_recreated_sfg_fft(schedule, 8) def test_radix_2_fft_8_points_one_output(self): sfg = radix_2_dif_fft(points=8) @@ -595,7 +610,10 @@ class TestHybridScheduler: "out5": 12, } assert schedule.schedule_time == 12 + _validate_recreated_sfg_fft(schedule, 8) + # This schedule that this test is checking against is faulty and will yield a non-working + # fft implementation, however, it is kept commented out for reference def test_radix_2_fft_8_points_specified_IO_times_cyclic(self): sfg = radix_2_dif_fft(points=8) @@ -676,6 +694,30 @@ class TestHybridScheduler: } assert schedule.schedule_time == 20 + # impulse input -> constant output + sim = Simulation(schedule.sfg, [Impulse()] + [0 for i in range(7)]) + sim.run_for(2) + assert np.allclose(sim.results["0"], [1, 0]) + assert np.allclose(sim.results["1"], [1, 0]) + assert np.allclose(sim.results["2"], [1, 0]) + assert np.allclose(sim.results["3"], [1, 0]) + assert np.allclose(sim.results["4"], [0, 1]) + assert np.allclose(sim.results["5"], [0, 1]) + assert np.allclose(sim.results["6"], [0, 1]) + assert np.allclose(sim.results["7"], [0, 1]) + + # constant input -> impulse (with weight=points) output + sim = Simulation(schedule.sfg, [Impulse() for i in range(8)]) + sim.run_for(2) + assert np.allclose(sim.results["0"], [8, 0]) + assert np.allclose(sim.results["1"], [0, 0]) + assert np.allclose(sim.results["2"], [0, 0]) + assert np.allclose(sim.results["3"], [0, 0]) + assert np.allclose(sim.results["4"], [0, 0]) + assert np.allclose(sim.results["5"], [0, 0]) + assert np.allclose(sim.results["6"], [0, 0]) + assert np.allclose(sim.results["7"], [0, 0]) + def test_radix_2_fft_8_points_specified_IO_times_non_cyclic(self): sfg = radix_2_dif_fft(points=8) @@ -749,6 +791,7 @@ class TestHybridScheduler: "out7": 24, } assert schedule.schedule_time == 24 + _validate_recreated_sfg_fft(schedule, 8) def test_ldlt_inverse_2x2(self): sfg = ldlt_matrix_inverse(N=2) @@ -1140,7 +1183,7 @@ class TestHybridScheduler: assert schedule.start_times[f"in{i}"] == i assert schedule.start_times[f"out{i}"] == 95 + i - # Too slow for pipeline right now + # too slow for pipeline timeout # def test_64_point_fft_custom_io_times(self): # POINTS = 64 # sfg = radix_2_dif_fft(POINTS) @@ -1166,7 +1209,7 @@ class TestHybridScheduler: # assert schedule.start_times[f"in{i}"] == i # assert ( # schedule.start_times[f"out{i}"] - # == schedule.get_max_non_io_end_time() + i + # == schedule.get_max_non_io_end_time() - 1 + i # ) def test_32_point_fft_custom_io_times_cyclic(self): @@ -1353,27 +1396,6 @@ class TestHybridScheduler: } assert schedule_4.schedule_time == 4 - def test_cyclic_scheduling_time_not_provided(self): - sfg = ldlt_matrix_inverse(N=2) - - sfg.set_latency_of_type(MADS.type_name(), 3) - sfg.set_latency_of_type(Reciprocal.type_name(), 2) - sfg.set_execution_time_of_type(MADS.type_name(), 1) - sfg.set_execution_time_of_type(Reciprocal.type_name(), 1) - - resources = {MADS.type_name(): 1, Reciprocal.type_name(): 1} - with pytest.raises( - ValueError, - match="Scheduling time must be provided when cyclic = True.", - ): - Schedule( - sfg, - scheduler=HybridScheduler( - max_resources=resources, - ), - cyclic=True, - ) - def test_resources_not_enough(self): sfg = ldlt_matrix_inverse(N=3) @@ -1873,10 +1895,61 @@ class TestHybridScheduler: "s53": 0, } - # - # schedule = Schedule( - # sfg, - # scheduler=HybridScheduler(max_concurrent_writes=2, max_concurrent_reads=2), - # schedule_time=30, - # cyclic=True, - # ) + +def _validate_recreated_sfg_filter(sfg: SFG, schedule: Schedule) -> None: + # compare the impulse response of the original sfg and recreated one + sim1 = Simulation(sfg, [Impulse()]) + sim1.run_for(1000) + sim2 = Simulation(schedule.sfg, [Impulse()]) + sim2.run_for(1000) + + spectrum_1 = abs(np.fft.fft(sim1.results['0'])) + spectrum_2 = abs(np.fft.fft(sim2.results['0'])) + assert np.allclose(spectrum_1, spectrum_2) + + +def _validate_recreated_sfg_fft(schedule: Schedule, points: int) -> None: + # impulse input -> constant output + sim = Simulation(schedule.sfg, [Impulse()] + [0 for i in range(points - 1)]) + sim.run_for(1) + for i in range(points): + assert np.allclose(sim.results[str(i)], 1) + + # constant input -> impulse (with weight=points) output + sim = Simulation(schedule.sfg, [Impulse() for i in range(points)]) + sim.run_for(1) + assert np.allclose(sim.results["0"], points) + for i in range(1, points): + assert np.allclose(sim.results[str(i)], 0) + + # sine input -> compare with numpy fft + n = np.linspace(0, 2 * np.pi, points) + waveform = np.sin(n) + input_samples = [Constant(waveform[i]) for i in range(points)] + sim = Simulation(schedule.sfg, input_samples) + sim.run_for(1) + exp_res = abs(np.fft.fft(waveform)) + res = sim.results + for i in range(points): + a = abs(res[str(i)]) + b = exp_res[i] + assert np.isclose(a, b) + + # multi-tone input -> compare with numpy fft + n = np.linspace(0, 2 * np.pi, points) + waveform = ( + 2 * np.sin(n) + + 1.3 * np.sin(0.9 * n) + + 0.9 * np.sin(0.6 * n) + + 0.35 * np.sin(0.3 * n) + + 2.4 * np.sin(0.1 * n) + ) + input_samples = [Constant(waveform[i]) for i in range(points)] + sim = Simulation(schedule.sfg, input_samples) + sim.run_for(1) + exp_res = np.fft.fft(waveform) + res = sim.results + for i in range(points): + a = res[str(i)] + b = exp_res[i] + assert np.isclose(a, b) diff --git a/test/unit/test_sfg.py b/test/unit/test_sfg.py index d042d071..471a8bde 100644 --- a/test/unit/test_sfg.py +++ b/test/unit/test_sfg.py @@ -1885,6 +1885,30 @@ class TestIterationPeriodBound: precedence_sfg_delays.set_latency_of_type('cmul', 3) assert precedence_sfg_delays.iteration_period_bound() == 10 + def test_fractional_value(self): + # Create the SFG for a digital filter (seen in an exam question from TSTE87). + x = Input() + t0 = Delay() + t1 = Delay(t0) + b = ConstantMultiplication(0.5, x) + d = ConstantMultiplication(0.5, t1) + a1 = Addition(x, d) + a = ConstantMultiplication(0.5, a1) + t2 = Delay(a1) + c = ConstantMultiplication(0.5, t2) + a2 = Addition(b, c) + a3 = Addition(a2, a) + t0.input(0).connect(a3) + y = Output(a2) + + sfg = SFG([x], [y]) + sfg.set_latency_of_type(Addition.type_name(), 1) + sfg.set_latency_of_type(ConstantMultiplication.type_name(), 1) + assert sfg.iteration_period_bound() == 4 / 2 + + sfg = sfg.insert_operation_before("t0", ConstantMultiplication(10)) + assert sfg.iteration_period_bound() == 5 / 2 + def test_no_delays(self, sfg_two_inputs_two_outputs): assert sfg_two_inputs_two_outputs.iteration_period_bound() == -1 -- GitLab