diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5704acb661c701a2c4c0f07afe14fb81986ba24d..606af51b44df4745ca63568185f3869bc5933686 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,15 +9,11 @@ before_script: - apt-get install -y libxcb-cursor-dev - python -m pip install --upgrade pip - python --version - - pip install .[$QT_API] - git fetch --tags + - pip install -v .[$QT_API,test] # - export CXXFLAGS='--coverage' - # Install without dependencies to make sure that requirements.txt is up-to-date - - pip install --no-deps -ve . - pip show b_asic - export QT_API=$QT_API - # Install test dependencies - - pip install .[test] - export PYTEST_QT_API=$QT_API .run-test: diff --git a/b_asic/architecture.py b/b_asic/architecture.py index c641e95a930d87e0b203a5db8ec5a595ebfb55fd..d7b41982f81e2f162057bbca884fef33bde25f67 100644 --- a/b_asic/architecture.py +++ b/b_asic/architecture.py @@ -260,7 +260,7 @@ class Resource(HardwareBlock): **kwargs Passed to :meth:`~b_asic.resources.ProcessCollection.plot`. """ - fig, ax = plt.subplots() + fig, ax = plt.subplots(layout="constrained") self.plot_content(ax, **kwargs) if title: fig.suptitle(title) @@ -294,7 +294,7 @@ class Resource(HardwareBlock): This is visible in enriched shells, but the object itself has no further meaning (it is a Matplotlib Figure). """ - fig, ax = plt.subplots() + fig, ax = plt.subplots(layout="constrained") self.plot_content(ax) return fig diff --git a/b_asic/gui_utils/mpl_window.py b/b_asic/gui_utils/mpl_window.py index 1a6db6785c5dcf473b767a23adf4b00994d6c2bd..2a3500114c4bb511f3d8259c82fdd3f717a4bf68 100644 --- a/b_asic/gui_utils/mpl_window.py +++ b/b_asic/gui_utils/mpl_window.py @@ -25,7 +25,7 @@ class MPLWindow(QDialog): self._dialog_layout = QVBoxLayout() self.setLayout(self._dialog_layout) - self._plot_fig = Figure(figsize=(5, 4), layout="compressed") + self._plot_fig = Figure(figsize=(5, 4), layout="constrained") self._plot_axes = self._plot_fig.subplots(*subplots) self._plot_canvas = FigureCanvas(self._plot_fig) diff --git a/b_asic/resources.py b/b_asic/resources.py index e54322bc8aa6799374ec4abf98ca1ba48a6e5bb9..6f979cee72e28331bef301fb52958c3eaf51cd02 100644 --- a/b_asic/resources.py +++ b/b_asic/resources.py @@ -580,7 +580,7 @@ class ProcessCollection: # Set up the Axes object if ax is None: - _, _ax = plt.subplots() + _, _ax = plt.subplots(layout="constrained") else: _ax = ax @@ -724,7 +724,7 @@ class ProcessCollection: title : str, optional Figure title. """ - fig, ax = plt.subplots() + fig, ax = plt.subplots(layout="constrained") self.plot( ax=ax, show_name=show_name, @@ -1122,7 +1122,7 @@ class ProcessCollection: This is automatically displayed in e.g. Jupyter Qt console. """ - fig, ax = plt.subplots() + fig, ax = plt.subplots(layout="constrained") self.plot(ax=ax, show_markers=False) f = io.StringIO() fig.savefig(f, format="svg") # type: ignore @@ -1628,7 +1628,7 @@ class ProcessCollection: title : str Figure title. """ - fig, axes = plt.subplots(3, 1) + fig, axes = plt.subplots(3, 1, layout="constrained") self.plot_port_accesses(axes) if title: fig.suptitle(title) diff --git a/b_asic/schedule.py b/b_asic/schedule.py index a20410d40df40616eaa76d5fb9f96d696f715864..9f349f0bcbb01b674ab9bd666ef03b8996384921 100644 --- a/b_asic/schedule.py +++ b/b_asic/schedule.py @@ -18,7 +18,6 @@ from matplotlib.lines import Line2D from matplotlib.patches import PathPatch, Polygon from matplotlib.path import Path from matplotlib.ticker import MaxNLocator -from matplotlib.transforms import Bbox, TransformedBbox from b_asic import Signal from b_asic._preferences import ( @@ -1067,24 +1066,66 @@ class Schedule: return operation_gap + y_location * (operation_height + operation_gap) def sort_y_locations_on_start_times(self): + """ + Sort the y-locations of the schedule based on start times of the operations. + + Inputs, outputs, dontcares, and sinks are located adjacent to the operations that + they are connected to. + """ for i, graph_id in enumerate( sorted(self._start_times, key=self._start_times.get) ): self.set_y_location(graph_id, i) for graph_id in self._start_times: op = cast(Operation, self._sfg.find_by_id(graph_id)) - if isinstance(op, Output): - self.move_y_location( - graph_id, - self.get_y_location(op.preceding_operations[0].graph_id) + 1, - True, - ) - if isinstance(op, DontCare): - self.move_y_location( - graph_id, - self.get_y_location(op.subsequent_operations[0].graph_id), - True, - ) + # Position Outputs and Sinks adjacent to the operation generating them + if isinstance(op, (Output, Sink)): + gen_op: Operation = op.preceding_operations[0] + if ( + gen_op.output_count == 1 + or op.input(0).signals[0].source.index >= gen_op.output_count / 2 + ): + # Single output operation generating or lower half of outputs + # Put below + self.move_y_location( + graph_id, + self.get_y_location(gen_op.graph_id) + 1, + True, + ) + else: + # Put above + self.move_y_location( + graph_id, + self.get_y_location(gen_op.graph_id), + True, + ) + # Position DontCares and Inputs adjacent to the operation using them + if isinstance(op, (DontCare, Input)): + # Find the "top" connected operation (for DontCare there is always one, but use general case there as well) + # TODO: Only check conconnected operations that do not have a lap + dest_ops = { + sub_op: self.get_y_location(sub_op.graph_id) + for sub_op in op.subsequent_operations + } + dest_op = min(dest_ops, key=dest_ops.get) + if ( + dest_op.input_count == 1 + or op.output(0).signals[0].destination.index + < dest_op.input_count / 2 + ): + # For single input operation consuming or upper half, position above + self.move_y_location( + graph_id, + self.get_y_location(dest_op.graph_id), + True, + ) + else: + # Position below + self.move_y_location( + graph_id, + self.get_y_location(dest_op.graph_id) + 1, + True, + ) def _plot_schedule(self, ax: Axes, operation_gap: float = OPERATION_GAP) -> None: """Draw the schedule.""" @@ -1192,12 +1233,10 @@ class Schedule: y = np.array(_y) xvalues = x + op_start_time xy = np.stack((xvalues, y + y_pos)) - p = ax.add_patch(Polygon(xy.T, fc=_LATENCY_COLOR)) - p.set_clip_box(TransformedBbox(Bbox([[0, 0], [1, 1]]), ax.transAxes)) + ax.add_patch(Polygon(xy.T, fc=_LATENCY_COLOR)) if any(xvalues > self.schedule_time) and not isinstance(operation, Output): xy = np.stack((xvalues - self.schedule_time, y + y_pos)) - p = ax.add_patch(Polygon(xy.T, fc=_LATENCY_COLOR)) - p.set_clip_box(TransformedBbox(Bbox([[0, 0], [1, 1]]), ax.transAxes)) + ax.add_patch(Polygon(xy.T, fc=_LATENCY_COLOR)) if isinstance(operation, Input): ax.annotate( graph_id, @@ -1224,7 +1263,7 @@ class Schedule: linewidth=3, ) if any(xvalues > self.schedule_time) and not isinstance( - operation, Output + operation, (Output, Sink) ): ax.plot( xvalues - self.schedule_time, @@ -1271,7 +1310,7 @@ class Schedule: + 1 + (OPERATION_GAP if operation_gap is None else operation_gap) ) - ax.axis([-1, self._schedule_time + 1, y_position_max, 0]) # Inverted y-axis + ax.axis([-0.8, self._schedule_time + 0.8, y_position_max, 0]) # Inverted y-axis ax.xaxis.set_major_locator(MaxNLocator(integer=True, min_n_ticks=1)) ax.axvline( 0, @@ -1335,8 +1374,8 @@ class Schedule: ------- The Matplotlib Figure. """ - height = len(self._start_times) * 0.3 + 2 - fig, ax = plt.subplots(figsize=(12, height)) + height = len(self._start_times) * 0.3 + 0.7 + fig, ax = plt.subplots(figsize=(12, height), layout="constrained") self._plot_schedule(ax, operation_gap=operation_gap) return fig @@ -1345,8 +1384,8 @@ class Schedule: Generate an SVG of the schedule. This is automatically displayed in e.g. Jupyter Qt console. """ - height = len(self._start_times) * 0.3 + 2 - fig, ax = plt.subplots(figsize=(12, height)) + height = len(self._start_times) * 0.3 + 0.7 + fig, ax = plt.subplots(figsize=(12, height), layout="constrained") self._plot_schedule(ax) buffer = io.StringIO() fig.savefig(buffer, format="svg") diff --git a/examples/auto_scheduling_with_custom_io_times.py b/examples/auto_scheduling_with_custom_io_times.py index 4df8bb7a332d2259b0163ded5952bc99cec0720b..d25fc8f1460b66244221d2b9a58cc5048f597ee5 100644 --- a/examples/auto_scheduling_with_custom_io_times.py +++ b/examples/auto_scheduling_with_custom_io_times.py @@ -27,15 +27,15 @@ sfg.set_execution_time_of_type(ConstantMultiplication.type_name(), 1) # %% # Generate an ASAP schedule for reference. -schedule = Schedule(sfg, scheduler=ASAPScheduler()) -schedule.show() +schedule1 = Schedule(sfg, scheduler=ASAPScheduler()) +schedule1.show() # %% # Generate a non-cyclic Schedule from HybridScheduler with custom IO times. resources = {Butterfly.type_name(): 1, ConstantMultiplication.type_name(): 1} input_times = {f"in{i}": i for i in range(points)} output_delta_times = {f"out{i}": i for i in range(points)} -schedule = Schedule( +schedule2 = Schedule( sfg, scheduler=HybridScheduler( resources, @@ -43,11 +43,11 @@ schedule = Schedule( output_delta_times=output_delta_times, ), ) -schedule.show() +schedule2.show() # %% # Generate a new Schedule with cyclic scheduling enabled. -schedule = Schedule( +schedule3 = Schedule( sfg, scheduler=HybridScheduler( resources, @@ -57,11 +57,11 @@ schedule = Schedule( schedule_time=14, cyclic=True, ) -schedule.show() +schedule3.show() # %% # Generate a new Schedule with even less scheduling time. -schedule = Schedule( +schedule4 = Schedule( sfg, scheduler=HybridScheduler( resources, @@ -71,11 +71,11 @@ schedule = Schedule( schedule_time=13, cyclic=True, ) -schedule.show() +schedule4.show() # %% # Try scheduling for 12 cycles, which gives full butterfly usage. -schedule = Schedule( +schedule5 = Schedule( sfg, scheduler=HybridScheduler( resources, @@ -85,4 +85,4 @@ schedule = Schedule( schedule_time=12, cyclic=True, ) -schedule.show() +schedule5.show() diff --git a/examples/latency_offset_scheduling.py b/examples/latency_offset_scheduling.py index 39c21b72346350ef9af0336433dcd12922275cbc..67fff73b44c8388c5af004d3cf648c52a47224a8 100644 --- a/examples/latency_offset_scheduling.py +++ b/examples/latency_offset_scheduling.py @@ -28,35 +28,35 @@ sfg # %% # Create an ASAP schedule for reference. -schedule = Schedule(sfg, scheduler=ASAPScheduler()) -schedule.show() +schedule1 = Schedule(sfg, scheduler=ASAPScheduler()) +schedule1.show() # %% # Create an ALAP schedule for reference. -schedule = Schedule(sfg, scheduler=ALAPScheduler()) -schedule.show() +schedule2 = Schedule(sfg, scheduler=ALAPScheduler()) +schedule2.show() # %% # Create a resource restricted schedule. -schedule = Schedule(sfg, scheduler=HybridScheduler()) -schedule.show() +schedule3 = Schedule(sfg, scheduler=HybridScheduler()) +schedule3.show() # %% # Create another schedule with shorter scheduling time by enabling cyclic. -schedule = Schedule( +schedule4 = Schedule( sfg, scheduler=HybridScheduler(), schedule_time=49, cyclic=True, ) -schedule.show() +schedule4.show() # %% # Push the schedule time to the rate limit for one MADS operator. -schedule = Schedule( +schedule5 = Schedule( sfg, scheduler=HybridScheduler(), schedule_time=15, cyclic=True, ) -schedule.show() +schedule5.show() diff --git a/examples/memory_constrained_scheduling.py b/examples/memory_constrained_scheduling.py index c1fad1dd8dbd0e5cda6be3f99c9775675f53e3fe..c459a9720cf546ad3684d6975b94ea8f26d25877 100644 --- a/examples/memory_constrained_scheduling.py +++ b/examples/memory_constrained_scheduling.py @@ -28,22 +28,22 @@ sfg.set_execution_time_of_type(ConstantMultiplication.type_name(), 1) # # %% # Generate an ASAP schedule for reference -schedule = Schedule(sfg, scheduler=ASAPScheduler()) -schedule.show() +schedule1 = Schedule(sfg, scheduler=ASAPScheduler()) +schedule1.show() # %% # Generate a PE constrained HybridSchedule resources = {Butterfly.type_name(): 1, ConstantMultiplication.type_name(): 1} -schedule = Schedule(sfg, scheduler=HybridScheduler(resources)) -schedule.show() +schedule2 = Schedule(sfg, scheduler=HybridScheduler(resources)) +schedule2.show() # %% Print the max number of read and write port accesses to non-direct memories -direct, mem_vars = schedule.get_memory_variables().split_on_length() +direct, mem_vars = schedule2.get_memory_variables().split_on_length() print("Max read ports:", mem_vars.read_ports_bound()) print("Max write ports:", mem_vars.write_ports_bound()) # %% -operations = schedule.get_operations() +operations = schedule2.get_operations() bfs = operations.get_by_type_name(Butterfly.type_name()) bfs.show(title="Butterfly executions") const_muls = operations.get_by_type_name(ConstantMultiplication.type_name()) @@ -59,7 +59,7 @@ mul_pe = ProcessingElement(const_muls, entity_name="mul") pe_in = ProcessingElement(inputs, entity_name='input') pe_out = ProcessingElement(outputs, entity_name='output') -mem_vars = schedule.get_memory_variables() +mem_vars = schedule2.get_memory_variables() mem_vars.show(title="All memory variables") direct, mem_vars = mem_vars.split_on_length() mem_vars.show(title="Non-zero time memory variables") @@ -89,21 +89,21 @@ arch # %% # Generate another HybridSchedule but this time constrain the amount of reads and writes to reduce the amount of memories resources = {Butterfly.type_name(): 1, ConstantMultiplication.type_name(): 1} -schedule = Schedule( +schedule3 = Schedule( sfg, scheduler=HybridScheduler( resources, max_concurrent_reads=2, max_concurrent_writes=2 ), ) -schedule.show() +schedule3.show() # %% Print the max number of read and write port accesses to non-direct memories -direct, mem_vars = schedule.get_memory_variables().split_on_length() +direct, mem_vars = schedule3.get_memory_variables().split_on_length() print("Max read ports:", mem_vars.read_ports_bound()) print("Max write ports:", mem_vars.write_ports_bound()) # %% Proceed to construct PEs and plot executions and non-direct memory variables -operations = schedule.get_operations() +operations = schedule3.get_operations() bfs = operations.get_by_type_name(Butterfly.type_name()) bfs.show(title="Butterfly executions") const_muls = operations.get_by_type_name(ConstantMultiplication.type_name()) diff --git a/test/unit/baseline_images/test_schedule/test__get_figure_no_execution_times.png b/test/unit/baseline_images/test_schedule/test__get_figure_no_execution_times.png index 79d2b4f1028378e9045b83c4a697ef6e00d801ec..4df499c2345dca70a074532c6af6050c2d91b9fc 100644 Binary files a/test/unit/baseline_images/test_schedule/test__get_figure_no_execution_times.png and b/test/unit/baseline_images/test_schedule/test__get_figure_no_execution_times.png differ diff --git a/test/unit/test_schedule.py b/test/unit/test_schedule.py index 6b499bd7b2fda26b744789f2ad59e9d922e74a54..f712a96e1879365b7188020c89dd5b28551af506 100644 --- a/test/unit/test_schedule.py +++ b/test/unit/test_schedule.py @@ -843,18 +843,18 @@ class TestYLocations: sfg_simple_filter.set_latency_of_type(ConstantMultiplication.type_name(), 2) schedule = Schedule(sfg_simple_filter, ASAPScheduler()) - assert schedule._y_locations == {"in0": 0, "cmul0": 1, "add0": 2, "out0": 3} + assert schedule._y_locations == {"in0": 1, "cmul0": 0, "add0": 2, "out0": 3} schedule.move_y_location("add0", 1, insert=True) - assert schedule._y_locations == {"in0": 0, "cmul0": 2, "add0": 1, "out0": 3} + assert schedule._y_locations == {"in0": 2, "cmul0": 0, "add0": 1, "out0": 3} schedule.move_y_location("out0", 1) - assert schedule._y_locations == {"in0": 0, "cmul0": 2, "add0": 1, "out0": 1} + assert schedule._y_locations == {"in0": 2, "cmul0": 0, "add0": 1, "out0": 1} def test_reset(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, ASAPScheduler()) - assert schedule._y_locations == {"in0": 0, "cmul0": 1, "add0": 2, "out0": 3} + assert schedule._y_locations == {"in0": 1, "cmul0": 0, "add0": 2, "out0": 3} schedule.reset_y_locations() assert schedule._y_locations["in0"] is None assert schedule._y_locations["cmul0"] is None