From 953eee43bc67760f4d53ac7481f211b7c5f820e6 Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson <oscar.gustafsson@gmail.com> Date: Sat, 15 Mar 2025 14:08:41 +0100 Subject: [PATCH] Use constrained layout for plots --- b_asic/architecture.py | 4 +- b_asic/gui_utils/mpl_window.py | 2 +- b_asic/resources.py | 8 +- b_asic/schedule.py | 90 ++++++++++++++----- .../auto_scheduling_with_custom_io_times.py | 20 ++--- examples/latency_offset_scheduling.py | 20 ++--- examples/memory_constrained_scheduling.py | 22 ++--- 7 files changed, 108 insertions(+), 58 deletions(-) diff --git a/b_asic/architecture.py b/b_asic/architecture.py index c641e95a..d7b41982 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 1a6db678..2a350011 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 e54322bc..6f979cee 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 a20410d4..070a1d09 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 ( @@ -1073,18 +1072,71 @@ class Schedule: self.set_y_location(graph_id, i) for graph_id in self._start_times: op = cast(Operation, self._sfg.find_by_id(graph_id)) + # Position Outputs adjacent to the operation generating them if isinstance(op, Output): - self.move_y_location( - graph_id, - self.get_y_location(op.preceding_operations[0].graph_id) + 1, - True, - ) + 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 adjacent to the operation using them if isinstance(op, DontCare): - self.move_y_location( - graph_id, - self.get_y_location(op.subsequent_operations[0].graph_id), - True, - ) + source_op: Operation = op.subsequent_operations[0] + if ( + source_op.input_count == 1 + or op.output(0).signals[0].destination.index + < source_op.input_count / 2 + ): + # For single input operation consuming or upper half, position above + self.move_y_location( + graph_id, + self.get_y_location(source_op.graph_id), + True, + ) + else: + # Position below + self.move_y_location( + graph_id, + self.get_y_location(source_op.graph_id) + 1, + True, + ) + # Position Inputs adjacent to the operation using them + if isinstance(op, Input): + if len(op.subsequent_operations) == 1: + # Single operation using input, same as DontDare + source_op: Operation = op.subsequent_operations[0] + if ( + source_op.input_count == 1 + or op.output(0).signals[0].destination.index + < source_op.input_count / 2 + ): + # For single input operation consuming or upper half, position above + self.move_y_location( + graph_id, + self.get_y_location(source_op.graph_id), + True, + ) + else: + # Position below + self.move_y_location( + graph_id, + self.get_y_location(source_op.graph_id) + 1, + True, + ) def _plot_schedule(self, ax: Axes, operation_gap: float = OPERATION_GAP) -> None: """Draw the schedule.""" @@ -1192,12 +1244,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, @@ -1271,7 +1321,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 +1385,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 +1395,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 4df8bb7a..d25fc8f1 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 39c21b72..67fff73b 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 c1fad1dd..c459a972 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()) -- GitLab