From c3793718baecd1004f6cb366410f47f710d10d94 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                    | 67 +++++++++++++++++++--------
 examples/latency_offset_scheduling.py | 20 ++++----
 5 files changed, 64 insertions(+), 37 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..aa075a20 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,48 @@ 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
+                    < gen_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),
+                        True,
+                    )
 
     def _plot_schedule(self, ax: Axes, operation_gap: float = OPERATION_GAP) -> None:
         """Draw the schedule."""
@@ -1192,12 +1221,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 +1298,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 +1362,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 +1372,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/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()
-- 
GitLab