From e0aa4e17afebde6a9201ee2c4fe4deebbd5b1f62 Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson <oscar.gustafsson@liu.se> Date: Wed, 16 Apr 2025 10:20:32 +0000 Subject: [PATCH] Docs and typing --- b_asic/__init__.py | 6 +- b_asic/architecture.py | 12 +++- b_asic/core_operations.py | 33 ++++++++-- b_asic/logger.py | 10 +-- b_asic/operation.py | 12 +++- b_asic/process.py | 4 +- b_asic/resources.py | 76 +++++++++++++++------- b_asic/schedule.py | 14 ++-- b_asic/scheduler.py | 76 ++++++++++++++++++---- b_asic/scheduler_gui/preferences_dialog.py | 13 ++-- b_asic/sfg_generators.py | 13 ++-- b_asic/signal_flow_graph.py | 4 +- docs_sphinx/api/scheduler.rst | 6 ++ docs_sphinx/scheduler_gui.rst | 2 +- 14 files changed, 201 insertions(+), 80 deletions(-) diff --git a/b_asic/__init__.py b/b_asic/__init__.py index 1d0af5f6..fe6b1cb0 100644 --- a/b_asic/__init__.py +++ b/b_asic/__init__.py @@ -1,5 +1,7 @@ -"""B-ASIC - Better ASIC Toolbox. -ASIC toolbox that simplifies circuit design and optimization. +""" +B-ASIC - Better ASIC Toolbox. + +A Python toolbox that simplifies implementation and optimization of static algorithms. """ # Python modules. diff --git a/b_asic/architecture.py b/b_asic/architecture.py index 9ceaacc7..a843a903 100644 --- a/b_asic/architecture.py +++ b/b_asic/architecture.py @@ -110,7 +110,9 @@ class HardwareBlock: @property def schedule_time(self) -> int: - """The schedule time for hardware block.""" + """ + The schedule time for hardware block. + """ raise NotImplementedError() def write_component_declaration(self, f: TextIOWrapper, indent: int = 1) -> None: @@ -182,12 +184,16 @@ class Resource(HardwareBlock): @property def input_count(self) -> int: - """Number of input ports.""" + """ + Number of input ports. + """ return self._input_count @property def output_count(self) -> int: - """Number of output ports.""" + """ + Number of output ports. + """ return self._output_count def _struct_def(self) -> str: diff --git a/b_asic/core_operations.py b/b_asic/core_operations.py index e5d9dc02..0b39ba9d 100644 --- a/b_asic/core_operations.py +++ b/b_asic/core_operations.py @@ -39,7 +39,9 @@ class Constant(AbstractOperation): is_constant = True def __init__(self, value: Num = 0, name: Name = ""): - """Construct a Constant operation with the given value.""" + """ + Construct a Constant operation with the given value. + """ super().__init__( input_count=0, output_count=1, @@ -57,12 +59,16 @@ class Constant(AbstractOperation): @property def value(self) -> Num: - """Get the constant value of this operation.""" + """ + Get the constant value of this operation. + """ return self.param("value") @value.setter def value(self, value: Num) -> None: - """Set the constant value of this operation.""" + """ + Set the constant value of this operation. + """ self.set_param("value", value) @property @@ -245,7 +251,9 @@ class Subtraction(AbstractOperation): latency_offsets: dict[str, int] | None = None, execution_time: int | None = None, ): - """Construct a Subtraction operation.""" + """ + Construct a Subtraction operation. + """ super().__init__( input_count=2, output_count=1, @@ -334,7 +342,9 @@ class AddSub(AbstractOperation): latency_offsets: dict[str, int] | None = None, execution_time: int | None = None, ): - """Construct an Addition/Subtraction operation.""" + """ + Construct an Addition/Subtraction operation. + """ super().__init__( input_count=2, output_count=1, @@ -355,12 +365,21 @@ class AddSub(AbstractOperation): @property def is_add(self) -> bool: - """Get if operation is an addition.""" + """ + Get if operation is an addition. + """ return self.param("is_add") @is_add.setter def is_add(self, is_add: bool) -> None: - """Set if operation is an addition.""" + """ + Set if operation is an addition. + + Parameters + ---------- + is_add : bool + If True, operation is an addition. If False, operation is a subtraction. + """ self.set_param("is_add", is_add) @property diff --git a/b_asic/logger.py b/b_asic/logger.py index 96f89df6..d279388f 100644 --- a/b_asic/logger.py +++ b/b_asic/logger.py @@ -59,8 +59,7 @@ def getLogger( name: str, filename: str | None = "", console_log_level: str = "warning" ) -> Logger: """ - This function creates console- and filehandler and from those, creates a logger - object. + Create console- and filehandler and from those, create a logger object. Parameters ---------- @@ -125,8 +124,11 @@ def handle_exceptions( exc_value: BaseException, exc_traceback: TracebackType | None, ) -> None: - """This function is a helper function to log uncaught exceptions. Install with: - `sys.excepthook = <module>.handle_exceptions`""" + """ + Helper function to log uncaught exceptions. + + Install with: `sys.excepthook = <module>.handle_exceptions` + """ if issubclass(exc_type, KeyboardInterrupt): sys.__excepthook__(exc_type, exc_value, exc_traceback) return diff --git a/b_asic/operation.py b/b_asic/operation.py index 955ce168..2ca298e9 100644 --- a/b_asic/operation.py +++ b/b_asic/operation.py @@ -56,18 +56,24 @@ class Operation(GraphComponent, SignalSourceProvider): @property @abstractmethod def input_count(self) -> int: - """Get the number of input ports.""" + """ + Get the number of input ports. + """ raise NotImplementedError @property @abstractmethod def output_count(self) -> int: - """Get the number of output ports.""" + """ + Get the number of output ports. + """ raise NotImplementedError @abstractmethod def input(self, index: int) -> InputPort: - """Get the input port at the given index.""" + """ + Get the input port at the given index. + """ raise NotImplementedError @abstractmethod diff --git a/b_asic/process.py b/b_asic/process.py index be4ccbe2..cb5d77ff 100644 --- a/b_asic/process.py +++ b/b_asic/process.py @@ -303,7 +303,7 @@ class MemoryVariable(MemoryProcess): super().__init__( write_time=write_time, life_times=list(reads.values()), - name=name, + name=name or write_port.name, ) @property @@ -378,7 +378,7 @@ class PlainMemoryVariable(MemoryProcess): __slots__ = ("_reads", "_read_ports", "_write_port") _reads: dict[int, int] _read_ports: list[int] - _write_port: OutputPort + _write_port: int def __init__( self, diff --git a/b_asic/resources.py b/b_asic/resources.py index 95b42d82..26a36336 100644 --- a/b_asic/resources.py +++ b/b_asic/resources.py @@ -918,7 +918,17 @@ class ProcessCollection: "greedy_graph_color", "ilp_graph_color", ] = "left_edge", - coloring_strategy: str = "saturation_largest_first", + coloring_strategy: Literal[ + 'largest_first', + 'random_sequential', + 'smallest_last', + 'independent_set', + 'connected_sequential_bfs', + 'connected_sequential_dfs', + 'connected_sequential', + 'saturation_largest_first', + 'DSATUR', + ] = "saturation_largest_first", max_colors: int | None = None, solver: PULP_CBC_CMD | GUROBI | None = None, ) -> list["ProcessCollection"]: @@ -950,9 +960,10 @@ class ProcessCollection: solver : PuLP MIP solver object, optional Only used if strategy is an ILP method. - Valid options are: - * PULP_CBC_CMD() - preinstalled with the package - * GUROBI() - required licence but likely faster + Valid options are + + * PULP_CBC_CMD() - preinstalled + * GUROBI() - license required, but likely faster Returns ------- @@ -969,7 +980,15 @@ class ProcessCollection: def split_on_ports( self, - strategy: str = "left_edge", + strategy: Literal[ + "ilp_graph_color", + "ilp_min_input_mux", + "greedy_graph_color", + "equitable_graph_color", + "left_edge", + "left_edge_min_pe_to_mem", + "left_edge_min_mem_to_pe", + ] = "left_edge", read_ports: int | None = None, write_ports: int | None = None, total_ports: int | None = None, @@ -986,35 +1005,43 @@ class ProcessCollection: ---------- strategy : str, default: "left_edge" The strategy used when splitting this :class:`ProcessCollection`. - Valid options are: - * "ilp_graph_color" - * "ilp_min_input_mux" - * "greedy_graph_color" - * "equitable_graph_color" - * "left_edge" - * "left_edge_min_pe_to_mem" - * "left_edge_min_mem_to_pe" + Valid options are + + * "ilp_graph_color" - ILP-based optimal graph coloring + * "ilp_min_input_mux" - ILP-based optimal graph coloring minimizing the number of input multiplexers + * "greedy_graph_color" + * "equitable_graph_color" + * "left_edge" + * "left_edge_min_pe_to_mem" + * "left_edge_min_mem_to_pe" + read_ports : int, optional The number of read ports used when splitting process collection based on memory variable access. + write_ports : int, optional The number of write ports used when splitting process collection based on memory variable access. + total_ports : int, optional The total number of ports used when splitting process collection based on memory variable access. - processing_elements : list of ProcessingElement, optional + + processing_elements : list of :class:`ProcessingElement`, optional The currently used PEs, - only required if strategy = "left_edge_min_mem_to_pe", + only required if *strategy* is "left_edge_min_mem_to_pe", "ilp_graph_color" or "ilp_min_input_mux". + max_colors : int, optional The maximum amount of colors to split based on, only required if strategy is an ILP method. + solver : PuLP MIP solver object, optional Only used if strategy is an ILP method. - Valid options are: - * PULP_CBC_CMD() - preinstalled with the package - * GUROBI() - required licence but likely faster + Valid options are + + * PULP_CBC_CMD() - preinstalled with the package + * GUROBI() - required licence but likely faster Returns ------- @@ -1128,13 +1155,12 @@ class ProcessCollection: sequence: list[Process], ) -> list["ProcessCollection"]: """ - Split this collection into multiple new collections by sequentially assigning - processes in the order of `sequence`. + Split this collection by sequentially assigning processes in the order of `sequence`. This method takes the processes from `sequence`, in order, and assigns them to to multiple new `ProcessCollection` based on port collisions in a first-come - first-served manner. The first `Process` in `sequence` is assigned first, and - the last `Process` in `sequence is assigned last. + first-served manner. The first :class:`Process` in `sequence` is assigned first, and + the last :class:`Process` in `sequence is assigned last. Parameters ---------- @@ -1147,14 +1173,14 @@ class ProcessCollection: total_ports : int The total number of ports used when splitting process collection based on memory variable access. - sequence : list of `Process` + sequence : list of :class:`Process` A list of the processes used to determine the order in which processes are assigned. Returns ------- - list of `ProcessCollection` - A set of new ProcessCollection objects with the process splitting. + list of :class:`ProcessCollection` + A list of new :class:`ProcessCollection` objects with the process splitting. """ if set(self.collection) != set(sequence): diff --git a/b_asic/schedule.py b/b_asic/schedule.py index 368a20f2..a3d8aaa1 100644 --- a/b_asic/schedule.py +++ b/b_asic/schedule.py @@ -655,21 +655,23 @@ class Schedule: """ self._sfg.set_execution_time_of_type_name(type_name, execution_time) - def set_latency_of_type_name(self, type_name: TypeName, latency: int) -> None: + def set_latency_of_type_name( + self, type_name: TypeName | GraphID, latency: int + ) -> None: """ Set the latency 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()``. + type_name : TypeName or GraphID + The type name of the operation, e.g., obtained as + ``Addition.type_name()`` or a specific GraphID of an operation. latency : int The latency of the operation. """ passed = True for op in self._sfg.operations: - if type_name == op.type_name() or type_name == op.graph_id: + if type_name in (op.type_name(), op.graph_id): change_in_latency = latency - op.latency if change_in_latency > (self.forward_slack(op.graph_id)): passed = False @@ -680,7 +682,7 @@ class Schedule: break if change_in_latency < 0 or passed: for op in self._sfg.operations: - if type_name == op.type_name() or type_name == op.graph_id: + if type_name in (op.type_name(), op.graph_id): cast(Operation, op).set_latency(latency) def move_y_location( diff --git a/b_asic/scheduler.py b/b_asic/scheduler.py index a50e390d..0bae94f3 100644 --- a/b_asic/scheduler.py +++ b/b_asic/scheduler.py @@ -29,7 +29,7 @@ class Scheduler(ABC): input_times : dict(GraphID, int), optional The times when inputs arrive. output_delta_times : dict(GraphID, int), optional - The relative time when outputs should be produced + The relative times when outputs should be produced. sort_y_location : bool, default: True If the y-position should be sorted based on start time of operations. """ @@ -223,7 +223,18 @@ class Scheduler(ABC): class ASAPScheduler(Scheduler): - """Scheduler that implements the as-soon-as-possible (ASAP) algorithm.""" + """ + Scheduler that implements the as-soon-as-possible (ASAP) algorithm. + + Parameters + ---------- + input_times : dict(GraphID, int), optional + The times when inputs arrive. + output_delta_times : dict(GraphID, int), optional + The relative times when outputs should be produced. + sort_y_location : bool, default: True + If the y-position should be sorted based on start time of operations. + """ def apply_scheduling(self, schedule: "Schedule") -> None: # Doc-string inherited @@ -307,7 +318,18 @@ class ASAPScheduler(Scheduler): class ALAPScheduler(Scheduler): - """Scheduler that implements the as-late-as-possible (ALAP) algorithm.""" + """ + Scheduler that implements the as-late-as-possible (ALAP) algorithm. + + Parameters + ---------- + input_times : dict(GraphID, int), optional + The times when inputs arrive. + output_delta_times : dict(GraphID, int), optional + The relative times when outputs should be produced. + sort_y_location : bool, default: True + If the y-position should be sorted based on start time of operations. + """ def apply_scheduling(self, schedule: "Schedule") -> None: # Doc-string inherited @@ -363,7 +385,7 @@ class ALAPScheduler(Scheduler): class ListScheduler(Scheduler): """ - List-based scheduler that schedules the operations with constraints. + List-based scheduler with optional constraints. .. admonition:: Important @@ -372,19 +394,21 @@ class ListScheduler(Scheduler): Parameters ---------- - sort_order : tuple[tuple[int, bool]] + 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 resources available to realize the schedule. max_concurrent_reads : int | None, optional - Max number of conccurent reads, by default None + Max number of conccurent reads. max_concurrent_writes : int | None, optional - Max number of conccurent writes, by default None - input_times : dict["GraphID", int] | None, optional - Specified input times, by default None - output_delta_times : dict["GraphID", int] | None, optional - Specified output delta times, by default None + Max number of conccurent writes. + input_times : dict(GraphID, int) | None, optional + The times when inputs arrive. + output_delta_times : dict(GraphID, int) | None, optional + The relative times when outputs should be produced. + sort_y_location : bool, default: True + If the y-position should be sorted based on start time of operations. """ __slots__ = ( @@ -426,9 +450,9 @@ class ListScheduler(Scheduler): max_concurrent_writes: int | None = None, input_times: dict["GraphID", int] | None = None, output_delta_times: dict["GraphID", int] | None = None, - sort_y_locations: bool = True, + sort_y_location: bool = True, ) -> None: - super().__init__(input_times, output_delta_times, sort_y_locations) + super().__init__(input_times, output_delta_times, sort_y_location) self._sort_order = sort_order if max_resources is not None: @@ -886,6 +910,28 @@ class ListScheduler(Scheduler): class RecursiveListScheduler(ListScheduler): + """ + List-based scheduler for recursive algorithms with optional constraints. + + .. admonition:: Info + + If the SFG does not have any recursive parts, use :class:`ListScheduler` instead. + + 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. + input_times : dict(GraphID, int) | None, optional + The times when inputs arrive. + output_delta_times : dict(GraphID, int) | None, optional + The relative times when outputs should be produced. + sort_y_location : bool, default: True + If the y-position should be sorted based on start time of operations. + """ + __slots__ = ('_recursive_ops', '_recursive_ops_set', '_remaining_recursive_ops') def __init__( @@ -894,12 +940,14 @@ class RecursiveListScheduler(ListScheduler): max_resources: dict[TypeName, int] | None = None, input_times: dict["GraphID", int] | None = None, output_delta_times: dict["GraphID", int] | None = None, + sort_y_location: bool = True, ) -> None: super().__init__( sort_order=sort_order, max_resources=max_resources, input_times=input_times, output_delta_times=output_delta_times, + sort_y_location=sort_y_location, ) def apply_scheduling(self, schedule: "Schedule") -> None: diff --git a/b_asic/scheduler_gui/preferences_dialog.py b/b_asic/scheduler_gui/preferences_dialog.py index 45d4eff9..3e671b4e 100644 --- a/b_asic/scheduler_gui/preferences_dialog.py +++ b/b_asic/scheduler_gui/preferences_dialog.py @@ -169,7 +169,7 @@ class PreferencesDialog(QWidget): layout.addWidget(groupbox) reset_font_button = ColorButton(QColor("silver")) - reset_font_button.setText("Reset All Font Settings") + reset_font_button.setText("Reset all font Settings") reset_font_button.pressed.connect(lambda: self.reset_font_clicked()) layout.addWidget(reset_font_button) @@ -216,7 +216,8 @@ class PreferencesDialog(QWidget): self.update_font() def create_color_button(self, color: ColorDataType) -> ColorButton: - """Create a colored button to be used to modify a certain color + """ + Create a colored button to be used to modify a certain color. Parameters ---------- @@ -308,7 +309,7 @@ class PreferencesDialog(QWidget): def color_button_clicked(self, color_type: ColorDataType) -> None: """ - Open a color dialog to select a color based on the specified color type + Open a color dialog to select a color based on the specified color type. Parameters ---------- @@ -379,7 +380,7 @@ class PreferencesDialog(QWidget): self._boldbutton.set_color(QColor("snow")) def update_font(self): - """Update font preferences based on current Font settings""" + """Update font preferences based on current Font settings.""" settings = QSettings() FONT.changed = not ( FONT.current_font == FONT.DEFAULT @@ -395,7 +396,7 @@ class PreferencesDialog(QWidget): self._parent.load_preferences() def font_color_clicked(self): - """Select a font color and update preferences""" + """Select a font color and update preferences.""" settings = QSettings() color = QColorDialog.getColor(FONT.color, self, "Select font color") if color.isValid(): @@ -406,7 +407,7 @@ class PreferencesDialog(QWidget): self._parent._graph._font_color_change(FONT.color) def reset_color_clicked(self): - """Reset the color settings""" + """Reset the color settings.""" settings = QSettings() reset_color_settings(settings) self._parent._color_changed_per_type = False diff --git a/b_asic/sfg_generators.py b/b_asic/sfg_generators.py index b7e21aaa..bd5eec42 100644 --- a/b_asic/sfg_generators.py +++ b/b_asic/sfg_generators.py @@ -271,7 +271,8 @@ def symmetric_fir( mult_properties: dict[str, int] | dict[str, dict[str, int]] | None = None, add_properties: dict[str, int] | dict[str, dict[str, int]] | None = None, ) -> SFG: - r"""Generate a signal flow graph of a symmetric FIR filter. + r""" + Generate a signal flow graph of a symmetric FIR filter. The *coefficients* parameter is a sequence of impulse response values of even length:: @@ -351,7 +352,7 @@ def direct_form_1_iir( mult_properties: dict[str, int] | dict[str, dict[str, int]] | None = None, add_properties: dict[str, int] | dict[str, dict[str, int]] | None = None, ) -> SFG: - """Generates a direct-form IIR filter of type I with coefficients a and b.""" + """Generate a direct-form IIR filter of type I with coefficients a and b.""" if len(a) < 2 or len(b) < 2: raise ValueError( "Size of coefficient lists a and b needs to contain at least 2 element." @@ -425,7 +426,7 @@ def direct_form_2_iir( mult_properties: dict[str, int] | dict[str, dict[str, int]] | None = None, add_properties: dict[str, int] | dict[str, dict[str, int]] | None = None, ) -> SFG: - """Generates a direct-form IIR filter of type II with coefficients a and b.""" + """Generate a direct-form IIR filter of type II with coefficients a and b.""" if len(a) < 2 or len(b) < 2: raise ValueError( "Size of coefficient lists a and b needs to contain at least 2 element." @@ -503,7 +504,8 @@ def direct_form_2_iir( def radix_2_dif_fft(points: int) -> SFG: - """Generates a radix-2 decimation-in-frequency FFT structure. + """ + Generate a radix-2 decimation-in-frequency FFT structure. Parameters ---------- @@ -548,7 +550,8 @@ def ldlt_matrix_inverse( mads_properties: dict[str, int] | dict[str, dict[str, int]] | None = None, reciprocal_properties: dict[str, int] | dict[str, dict[str, int]] | None = None, ) -> SFG: - """Generates an SFG for the LDLT matrix inverse algorithm. + """ + Generate an SFG for the LDLT matrix inverse algorithm. Parameters ---------- diff --git a/b_asic/signal_flow_graph.py b/b_asic/signal_flow_graph.py index 859e3647..349e0e8e 100644 --- a/b_asic/signal_flow_graph.py +++ b/b_asic/signal_flow_graph.py @@ -13,7 +13,7 @@ from fractions import Fraction from io import StringIO from math import ceil from queue import PriorityQueue -from typing import ClassVar, Literal, Union, cast +from typing import Literal, Union, cast import numpy as np from graphviz import Digraph @@ -797,7 +797,7 @@ class SFG(AbstractOperation): The source operation GraphID to connect to. new_operation : Operation The new operation, e.g. Multiplication. - port : Optional[int] + port : int, optional The number of the InputPort before which the new operation shall be inserted. """ diff --git a/docs_sphinx/api/scheduler.rst b/docs_sphinx/api/scheduler.rst index 8d3c52dd..a81c19a4 100644 --- a/docs_sphinx/api/scheduler.rst +++ b/docs_sphinx/api/scheduler.rst @@ -3,6 +3,12 @@ ******************** :mod:`.scheduler` + +.. inheritance-diagram:: b_asic.operation + :parts: 1 + :top-classes: b_asic.scheduler.scheduler + + .. automodule:: b_asic.scheduler :members: :undoc-members: diff --git a/docs_sphinx/scheduler_gui.rst b/docs_sphinx/scheduler_gui.rst index 03ead85c..fed5c453 100644 --- a/docs_sphinx/scheduler_gui.rst +++ b/docs_sphinx/scheduler_gui.rst @@ -38,7 +38,7 @@ scheduler\_gui.operation\_item module :show-inheritance: scheduler\_gui.preferences\_dialog module -------------------------------------- +----------------------------------------- .. automodule:: b_asic.scheduler_gui.preferences_dialog :members: -- GitLab