diff --git a/b_asic/operation.py b/b_asic/operation.py index 0fe66840bcde65e35bb2c24de3b7a9e3ac32b42e..85b3d09c0e01bf15d475feaf172793e9bbb98bca 100644 --- a/b_asic/operation.py +++ b/b_asic/operation.py @@ -240,15 +240,27 @@ class Operation(GraphComponent, SignalSourceProvider): ) -> Number: """ Evaluate the output at the given index of this operation with the given input values. - The *results* parameter will be used to store any results (including intermediate results) - for caching. - The *delays* parameter will be used to get the current value of any intermediate delays - that are encountered, and be updated with their new values. - The *prefix* parameter will be used as a prefix for the key string when storing results/delays. - The *bits_override* parameter specifies a word length override when truncating inputs - which ignores the word length specified by the input signal. - The *truncate* parameter specifies whether input truncation should be enabled in the first - place. If set to False, input values will be used directly without any bit truncation. + + Parameters + ---------- + index : int + Which output to return the value for. + input_values : array of float or complex + The input values. + results : MutableResultMap. optional + Used to store any results (including intermediate results) + for caching. + delays : MutableDelayMap. optional + Used to get the current value of any intermediate delay elements + that are encountered, and be updated with their new values. + prefix : str, optional + Used as a prefix for the key string when storing results/delays. + bits_override ; int, optional + Specifies a word length override when truncating inputs + which ignores the word length specified by the input signal. + truncate : bool, default: True + Specifies whether input truncation should be enabled in the first + place. If set to False, input values will be used directly without any bit truncation. See also ======== @@ -263,7 +275,10 @@ class Operation(GraphComponent, SignalSourceProvider): ) -> Sequence[Optional[Number]]: """ Get all current outputs of this operation, if available. - See current_output for more information. + + See also + ======== + current_output """ raise NotImplementedError @@ -1055,8 +1070,8 @@ class AbstractOperation(Operation, AbstractGraphComponent): def get_io_coordinates( self, ) -> Tuple[List[List[float]], List[List[float]]]: - self._check_all_latencies_set() # Doc-string inherited + self._check_all_latencies_set() input_coords = [ [ self.inputs[k].latency_offset, diff --git a/b_asic/schedule.py b/b_asic/schedule.py index 129ce92b3105218596f969d747e21ed3bfcb84fc..c74ac5f381115d2bfa12e9391c6d1c053f45d6a7 100644 --- a/b_asic/schedule.py +++ b/b_asic/schedule.py @@ -11,6 +11,8 @@ from typing import Dict, List, Optional, Tuple, cast import matplotlib.pyplot as plt import numpy as np +from matplotlib.axes import Axes +from matplotlib.figure import Figure from matplotlib.lines import Line2D from matplotlib.patches import PathPatch, Polygon from matplotlib.path import Path @@ -40,7 +42,20 @@ _SIGNAL_COLOR = tuple(c / 255 for c in SIGNAL_COLOR) class Schedule: - """Schedule of an SFG with scheduled Operations.""" + """ + Schedule of an SFG with scheduled Operations. + + Parameters + ---------- + sfg : SFG + The signal flow graph to schedule. + schedule_time : int, optional + 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 + The scheduling algorithm to use. Currently, only "ASAP" is supported. + """ _sfg: SFG _start_times: Dict[GraphID, int] @@ -82,7 +97,7 @@ class Schedule: def start_time_of_operation(self, graph_id: GraphID) -> int: """ - Get the start time of the operation with the specified by *graph_id*. + Return the start time of the operation with the specified by *graph_id*. """ if graph_id not in self._start_times: raise ValueError( @@ -91,7 +106,7 @@ class Schedule: return self._start_times[graph_id] def get_max_end_time(self) -> int: - """Returns the current maximum end time among all operations.""" + """Return the current maximum end time among all operations.""" max_end_time = 0 for graph_id, op_start_time in self._start_times.items(): op = cast(Operation, self._sfg.find_by_id(graph_id)) @@ -103,6 +118,16 @@ class Schedule: return max_end_time def forward_slack(self, graph_id: GraphID) -> int: + """ + + Parameters + ---------- + graph_id + + Returns + ------- + The number of time steps the operation with *graph_id* can ba moved forward in time. + """ if graph_id not in self._start_times: raise ValueError( f"No operation with graph_id {graph_id} in schedule" @@ -137,6 +162,16 @@ class Schedule: return ret def backward_slack(self, graph_id: GraphID) -> int: + """ + + Parameters + ---------- + graph_id + + Returns + ------- + The number of time steps the operation with *graph_id* can ba moved backward in time. + """ if graph_id not in self._start_times: raise ValueError( f"No operation with graph_id {graph_id} in schedule" @@ -181,6 +216,18 @@ class Schedule: raise NotImplementedError def set_schedule_time(self, time: int) -> "Schedule": + """ + Set a new schedule time. + + Parameters + ---------- + time : int + The new schedule time. If it is too short, a ValueError will be raised. + + See also + -------- + get_max_time + """ if time < self.get_max_end_time(): raise ValueError( "New schedule time ({time})to short, minimum:" @@ -359,6 +406,7 @@ class Schedule: delay_list = self._sfg.find_by_type_name(Delay.type_name()) def _schedule_asap(self) -> None: + """Schedule the operations using as-soon-as-possible scheduling.""" pl = self._sfg.get_precedence_list() if len(pl) < 2: @@ -479,9 +527,11 @@ class Schedule: return operation_gap + y_location * (operation_height + operation_gap) def _plot_schedule(self, ax, operation_gap=None): + """Draw the schedule.""" line_cache = [] def _draw_arrow(start, end, name="", laps=0): + """Draw an arrow from *start* to *end*.""" if end[0] < start[0] or laps > 0: # Wrap around if start not in line_cache: line = Line2D( @@ -564,6 +614,7 @@ class Schedule: def _draw_offset_arrow( start, end, start_offset, end_offset, name="", laps=0 ): + """Draw an arrow from *start* to *end*, but with an offset.""" _draw_arrow( [start[0] + start_offset[0], start[1] + start_offset[1]], [end[0] + end_offset[0], end[1] + end_offset[1]], @@ -576,7 +627,7 @@ class Schedule: ax.set_axisbelow(True) ax.grid() for graph_id, op_start_time in self._start_times.items(): - ypos = -self._get_y_position(graph_id, operation_gap=operation_gap) + ypos = self._get_y_position(graph_id, operation_gap=operation_gap) op = self._sfg.find_by_id(graph_id) # Rewrite to make better use of NumPy latency_coords, execution_time_coords = op.get_plot_coordinates() @@ -602,7 +653,7 @@ class Schedule: for graph_id, op_start_time in self._start_times.items(): op = self._sfg.find_by_id(graph_id) _, out_coords = op.get_io_coordinates() - source_ypos = -self._get_y_position( + source_ypos = self._get_y_position( graph_id, operation_gap=operation_gap ) @@ -610,7 +661,7 @@ class Schedule: for output_signal in output_port.signals: dest_op = output_signal.destination.operation dest_start_time = self._start_times[dest_op.graph_id] - dest_ypos = -self._get_y_position( + dest_ypos = self._get_y_position( dest_op.graph_id, operation_gap=operation_gap ) ( @@ -633,35 +684,73 @@ class Schedule: # Get operation with maximum position max_pos_graph_id = max(self._y_locations, key=self._y_locations.get) - yposmin = -self._get_y_position( - max_pos_graph_id, operation_gap=operation_gap - ) - (OPERATION_GAP if operation_gap is None else operation_gap) - ax.axis([-1, self._schedule_time + 1, yposmin, 1]) + yposmax = ( + self._get_y_position(max_pos_graph_id, operation_gap=operation_gap) + + 1 + + (OPERATION_GAP if operation_gap is None else operation_gap) + ) + ax.axis([-1, self._schedule_time + 1, yposmax, 0]) # Inverted y-axis ax.xaxis.set_major_locator(MaxNLocator(integer=True)) ax.add_line( - Line2D([0, 0], [yposmin, 1], linestyle="--", color="black") + Line2D([0, 0], [0, yposmax], linestyle="--", color="black") ) ax.add_line( Line2D( [self._schedule_time, self._schedule_time], - [yposmin, 1], + [0, yposmax], linestyle="--", color="black", ) ) def _reset_y_locations(self): + """Reset all the y-locations in the schedule to None""" self._y_locations = self._y_locations = defaultdict(lambda: None) - def plot_schedule(self, operation_gap=None) -> None: + def plot_in_axes(self, ax: Axes, operation_gap: float = None) -> None: + """ + Plot the schedule in a :class:`matplotlib.axes.Axes` or subclass. + + Parameters + ---------- + ax : matplotlib.axes.Axes + The :class:`matplotlib.axes.Axes` to plot in. + operation_gap : float, optional + The vertical distance between operations in the schedule. The height of the operation is always 1. + """ + + def plot(self, operation_gap: float = None) -> None: + """ + Plot the schedule. Will display based on the current Matplotlib backend. + + Parameters + ---------- + operation_gap : float, optional + The vertical distance between operations in the schedule. The height of the operation is always 1. + """ self._get_figure(operation_gap=operation_gap).show() - def _get_figure(self, operation_gap=None) -> None: + def _get_figure(self, operation_gap: float = None) -> Figure: + """ + Create a Figure and an Axes and plot schedule in the Axes. + + Parameters + ---------- + operation_gap : float, optional + The vertical distance between operations in the schedule. The height of the operation is always 1. + + Returns + ------- + The Matplotlib Figure. + """ fig, ax = plt.subplots() self._plot_schedule(ax, operation_gap=operation_gap) return fig def _repr_svg_(self): + """ + Generate an SVG of the schedule. This is automatically displayed in e.g. Jupyter Qt console. + """ fig, ax = plt.subplots() self._plot_schedule(ax) f = io.StringIO() diff --git a/b_asic/scheduler_gui/main_window.py b/b_asic/scheduler_gui/main_window.py index 41b59263cfce8a3657f464b97a05d850503495aa..cdea2fb88cf890f51cd0a733fa5c47c6a7fd1516 100644 --- a/b_asic/scheduler_gui/main_window.py +++ b/b_asic/scheduler_gui/main_window.py @@ -170,7 +170,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): # TODO: remove if self.schedule is None: return - self.schedule.plot_schedule() + self.schedule.plot() if self._graph is not None: print(f"filtersChildEvents(): {self._graph.filtersChildEvents()}") # self._printButtonPressed('callback_pushButton()') diff --git a/b_asic/signal_flow_graph.py b/b_asic/signal_flow_graph.py index 5a42494b57a79eaf4b289d23e510c6732aca496b..af39c97849a1ff1013a821f969f343bbef537656 100644 --- a/b_asic/signal_flow_graph.py +++ b/b_asic/signal_flow_graph.py @@ -78,10 +78,37 @@ class GraphIDGenerator: class SFG(AbstractOperation): """ - Signal flow graph. + Construct an SFG given its inputs and outputs. Contains a set of connected operations, forming a new operation. Used as a base for simulation, scheduling, etc. + + Inputs/outputs may be specified using either Input/Output operations + directly with the *inputs*/*outputs* parameters, or using signals with the + *input_signals*/*output_signals parameters*. If signals are used, the + corresponding Input/Output operations will be created automatically. + + The *id_number_offset* parameter specifies what number graph IDs will be + offset by for each new graph component type. IDs start at 1 by default, + so the default offset of 0 will result in IDs like "c1", "c2", etc. + while an offset of 3 will result in "c4", "c5", etc. + + Parameters + ---------- + inputs : array of Input, optional + + outputs : array of Output, optional + + input_signals : array of Signal, optional + + output_signals : array of Signal, optional + + id_number_offset : GraphIDNumber, optional + + name : Name, optional + + input_sources : + """ _components_by_id: Dict[GraphID, GraphComponent] @@ -110,20 +137,6 @@ class SFG(AbstractOperation): Sequence[Optional[SignalSourceProvider]] ] = None, ): - """ - Construct an SFG given its inputs and outputs. - - Inputs/outputs may be specified using either Input/Output operations - directly with the inputs/outputs parameters, or using signals with the - input_signals/output_signals parameters. If signals are used, the - corresponding Input/Output operations will be created automatically. - - The id_number_offset parameter specifies what number graph IDs will be - offset by for each new graph component type. IDs start at 1 by default, - so the default offset of 0 will result in IDs like "c1", "c2", etc. - while an offset of 3 will result in "c4", "c5", etc. - """ - input_signal_count = 0 if input_signals is None else len(input_signals) input_operation_count = 0 if inputs is None else len(inputs) output_signal_count = ( @@ -311,7 +324,7 @@ class SFG(AbstractOperation): ) def __str__(self) -> str: - """Get a string representation of this SFG.""" + """Return a string representation of this SFG.""" string_io = StringIO() string_io.write(super().__str__() + "\n") string_io.write("Internal Operations:\n") @@ -329,7 +342,7 @@ class SFG(AbstractOperation): self, *src: Optional[SignalSourceProvider], name: Name = Name("") ) -> "SFG": """ - Get a new independent SFG instance that is identical to this SFG + Return a new independent SFG instance that is identical to this SFG except without any of its external connections. """ return SFG( @@ -342,6 +355,7 @@ class SFG(AbstractOperation): @classmethod def type_name(cls) -> TypeName: + # doc-string inherited. return TypeName("sfg") def evaluate(self, *args): @@ -359,6 +373,7 @@ class SFG(AbstractOperation): bits_override: Optional[int] = None, truncate: bool = True, ) -> Number: + # doc-string inherited if index < 0 or index >= self.output_count: raise IndexError( "Output index out of range (expected" @@ -476,6 +491,18 @@ class SFG(AbstractOperation): return self def inputs_required_for_output(self, output_index: int) -> Iterable[int]: + """ + Return which inputs that the output depends on. + + Parameters + ---------- + output_index : int + The output index. + + Returns + ------- + A list of inputs that are required to compute the output with the given *output_index*. + """ if output_index < 0 or output_index >= self.output_count: raise IndexError( "Output index out of range (expected" @@ -521,7 +548,8 @@ class SFG(AbstractOperation): @property def id_number_offset(self) -> GraphIDNumber: - """Get the graph id number offset of the graph id generator for this SFG. + """ + Get the graph id number offset of the graph id generator for this SFG. """ return self._graph_id_generator.id_number_offset @@ -763,7 +791,7 @@ class SFG(AbstractOperation): return self._precedence_list - def show_precedence_graph(self) -> None: + def show(self) -> None: self.precedence_graph().view() def precedence_graph(self) -> Digraph: @@ -952,14 +980,32 @@ class SFG(AbstractOperation): return self._operations_topological_order def set_latency_of_type(self, type_name: TypeName, latency: int) -> None: - """Set the latency of all components with the given type name.""" + """ + Set the latency of all components with the given type name. + + Parameters + ---------- + type_name : TypeName + The type name of the operation. For example, obtained as ``Addition.type_name()``. + latency : int + The latency of the operation. + + """ for op in self.find_by_type_name(type_name): cast(Operation, op).set_latency(latency) def set_execution_time_of_type( self, type_name: TypeName, execution_time: int ) -> None: - """Set the execution time of all components with the given type name. + """ + Set the execution time of all operations with the given type name. + + Parameters + ---------- + type_name : TypeName + The type name of the operation. For example, obtained as ``Addition.type_name()``. + execution_time : int + The execution time of the operation. """ for op in self.find_by_type_name(type_name): cast(Operation, op).execution_time = execution_time @@ -967,7 +1013,15 @@ class SFG(AbstractOperation): def set_latency_offsets_of_type( self, type_name: TypeName, latency_offsets: Dict[str, int] ) -> None: - """Set the latency offset of all components with the given type name. + """ + Set the latency offsets of all operations with the given type name. + + Parameters + ---------- + type_name : TypeName + The type name of the operation. For example, obtained as ``Addition.type_name()``. + latency_offsets : {"in1": int, ...} + The latency offsets of the inputs and outputs. """ for op in self.find_by_type_name(type_name): cast(Operation, op).set_latency_offsets(latency_offsets) diff --git a/b_asic/signal_generator.py b/b_asic/signal_generator.py index 466e1f1e94a6bd988fe0f444cbd1aa5705331c98..be4543eb04b6d275e65560442a05fad3688e5dc0 100644 --- a/b_asic/signal_generator.py +++ b/b_asic/signal_generator.py @@ -10,6 +10,12 @@ from typing import Callable, Sequence class SignalGenerator: + """ + Base class for signal generators. + + Handles operator overloading and defined the ``__call__`` method that should be overridden. + """ + def __call__(self, time: int) -> complex: raise NotImplementedError diff --git a/examples/secondorderdirectformiir.py b/examples/secondorderdirectformiir.py index 2f2536cfcf70ccaf59f1c3b3337108ff96d024ec..f6aefffe3a8f84b005af7d0e154c0519b67d6262 100644 --- a/examples/secondorderdirectformiir.py +++ b/examples/secondorderdirectformiir.py @@ -42,4 +42,4 @@ sfg.set_execution_time_of_type(Addition.type_name(), 1) # Create schedule schedule = Schedule(sfg, cyclic=True) -schedule.plot_schedule() +schedule.plot() diff --git a/examples/threepointwinograddft.py b/examples/threepointwinograddft.py index 2d6614678f947a6f2c531b968e66d432fc97cde9..7e1fd5378b72e5c55ccfdef77a9cda8d93cb742f 100644 --- a/examples/threepointwinograddft.py +++ b/examples/threepointwinograddft.py @@ -53,4 +53,4 @@ sfg.set_execution_time_of_type(Subtraction.type_name(), 1) # %% # Generate schedule schedule = Schedule(sfg, cyclic=True) -schedule.plot_schedule() +schedule.plot() diff --git a/logo.svg b/logo.svg index 1133f65086fbc045ef7ee4a0f444a7e6530c756c..6d43369e921a58c0dc4deb10264192b40e628b22 100644 --- a/logo.svg +++ b/logo.svg @@ -10,8 +10,8 @@ inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" sodipodi:docname="logo.svg" inkscape:export-filename="icon_logo.png" - inkscape:export-xdpi="5.3303123" - inkscape:export-ydpi="5.3303123" + inkscape:export-xdpi="5.1594224" + inkscape:export-ydpi="5.1594224" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -28,7 +28,7 @@ inkscape:document-units="mm" showgrid="false" inkscape:zoom="0.4204826" - inkscape:cx="1055.9295" + inkscape:cx="511.31723" inkscape:cy="709.89857" inkscape:window-width="1200" inkscape:window-height="1896" diff --git a/test/baseline/test__get_figure_no_execution_times.png b/test/baseline/test__get_figure_no_execution_times.png index b68cadd183a9638a0596a4a58dd3b331d58e2332..329b3064a06dd922b83dfaa873fd8da2bc9021d6 100644 Binary files a/test/baseline/test__get_figure_no_execution_times.png and b/test/baseline/test__get_figure_no_execution_times.png differ